// SPDX-License-Identifier: GPL-2.0+ /* * DMI based code to deal with broken DSDTs on X86 tablets which ship with * Android as (part of) the factory image. The factory kernels shipped on these * devices typically have a bunch of things hardcoded, rather than specified * in their DSDT. * * Copyright (C) 2021 Hans de Goede */ #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt #include #include #include #include #include #include #include #include #include #include #include #include #include /* For gpio_get_desc() which is EXPORT_SYMBOL_GPL() */ #include "../../gpio/gpiolib.h" /* * Helper code to get Linux IRQ numbers given a description of the IRQ source * (either IOAPIC index, or GPIO chip name + pin-number). */ enum x86_acpi_irq_type { X86_ACPI_IRQ_TYPE_NONE, X86_ACPI_IRQ_TYPE_APIC, X86_ACPI_IRQ_TYPE_GPIOINT, X86_ACPI_IRQ_TYPE_PMIC, }; struct x86_acpi_irq_data { char *chip; /* GPIO chip label (GPIOINT) or PMIC ACPI path (PMIC) */ enum x86_acpi_irq_type type; enum irq_domain_bus_token domain; int index; int trigger; /* ACPI_EDGE_SENSITIVE / ACPI_LEVEL_SENSITIVE */ int polarity; /* ACPI_ACTIVE_HIGH / ACPI_ACTIVE_LOW / ACPI_ACTIVE_BOTH */ }; static int x86_acpi_irq_helper_gpiochip_find(struct gpio_chip *gc, void *data) { return gc->label && !strcmp(gc->label, data); } static int x86_acpi_irq_helper_get(const struct x86_acpi_irq_data *data) { struct irq_fwspec fwspec = { }; struct irq_domain *domain; struct acpi_device *adev; struct gpio_desc *gpiod; struct gpio_chip *chip; unsigned int irq_type; acpi_handle handle; acpi_status status; int irq, ret; switch (data->type) { case X86_ACPI_IRQ_TYPE_APIC: irq = acpi_register_gsi(NULL, data->index, data->trigger, data->polarity); if (irq < 0) pr_err("error %d getting APIC IRQ %d\n", irq, data->index); return irq; case X86_ACPI_IRQ_TYPE_GPIOINT: /* Like acpi_dev_gpio_irq_get(), but without parsing ACPI resources */ chip = gpiochip_find(data->chip, x86_acpi_irq_helper_gpiochip_find); if (!chip) { pr_err("error cannot find GPIO chip %s\n", data->chip); return -ENODEV; } gpiod = gpiochip_get_desc(chip, data->index); if (IS_ERR(gpiod)) { ret = PTR_ERR(gpiod); pr_err("error %d getting GPIO %s %d\n", ret, data->chip, data->index); return ret; } irq = gpiod_to_irq(gpiod); if (irq < 0) { pr_err("error %d getting IRQ %s %d\n", irq, data->chip, data->index); return irq; } irq_type = acpi_dev_get_irq_type(data->trigger, data->polarity); if (irq_type != IRQ_TYPE_NONE && irq_type != irq_get_trigger_type(irq)) irq_set_irq_type(irq, irq_type); return irq; case X86_ACPI_IRQ_TYPE_PMIC: status = acpi_get_handle(NULL, data->chip, &handle); if (ACPI_FAILURE(status)) { pr_err("error could not get %s handle\n", data->chip); return -ENODEV; } acpi_bus_get_device(handle, &adev); if (!adev) { pr_err("error could not get %s adev\n", data->chip); return -ENODEV; } fwspec.fwnode = acpi_fwnode_handle(adev); domain = irq_find_matching_fwspec(&fwspec, data->domain); if (!domain) { pr_err("error could not find IRQ domain for %s\n", data->chip); return -ENODEV; } return irq_create_mapping(domain, data->index); default: return 0; } } struct x86_i2c_client_info { struct i2c_board_info board_info; char *adapter_path; struct x86_acpi_irq_data irq_data; }; struct x86_serdev_info { const char *ctrl_hid; const char *ctrl_uid; const char *ctrl_devname; /* * ATM the serdev core only supports of or ACPI matching; and sofar all * Android x86 tablets DSDTs have usable serdev nodes, but sometimes * under the wrong controller. So we just tie the existing serdev ACPI * node to the right controller. */ const char *serdev_hid; }; struct x86_dev_info { const char * const *modules; struct gpiod_lookup_table **gpiod_lookup_tables; const struct x86_i2c_client_info *i2c_client_info; const struct platform_device_info *pdev_info; const struct x86_serdev_info *serdev_info; int i2c_client_count; int pdev_count; int serdev_count; }; /* * When booted with the BIOS set to Android mode the Chuwi Hi8 (CWI509) DSDT * contains a whole bunch of bogus ACPI I2C devices and is missing entries * for the touchscreen and the accelerometer. */ static const struct property_entry chuwi_hi8_gsl1680_props[] = { PROPERTY_ENTRY_U32("touchscreen-size-x", 1665), PROPERTY_ENTRY_U32("touchscreen-size-y", 1140), PROPERTY_ENTRY_BOOL("touchscreen-swapped-x-y"), PROPERTY_ENTRY_BOOL("silead,home-button"), PROPERTY_ENTRY_STRING("firmware-name", "gsl1680-chuwi-hi8.fw"), { } }; static const struct software_node chuwi_hi8_gsl1680_node = { .properties = chuwi_hi8_gsl1680_props, }; static const char * const chuwi_hi8_mount_matrix[] = { "1", "0", "0", "0", "-1", "0", "0", "0", "1" }; static const struct property_entry chuwi_hi8_bma250e_props[] = { PROPERTY_ENTRY_STRING_ARRAY("mount-matrix", chuwi_hi8_mount_matrix), { } }; static const struct software_node chuwi_hi8_bma250e_node = { .properties = chuwi_hi8_bma250e_props, }; static const struct x86_i2c_client_info chuwi_hi8_i2c_clients[] __initconst = { { /* Silead touchscreen */ .board_info = { .type = "gsl1680", .addr = 0x40, .swnode = &chuwi_hi8_gsl1680_node, }, .adapter_path = "\\_SB_.I2C4", .irq_data = { .type = X86_ACPI_IRQ_TYPE_APIC, .index = 0x44, .trigger = ACPI_EDGE_SENSITIVE, .polarity = ACPI_ACTIVE_HIGH, }, }, { /* BMA250E accelerometer */ .board_info = { .type = "bma250e", .addr = 0x18, .swnode = &chuwi_hi8_bma250e_node, }, .adapter_path = "\\_SB_.I2C3", .irq_data = { .type = X86_ACPI_IRQ_TYPE_GPIOINT, .chip = "INT33FC:02", .index = 23, .trigger = ACPI_LEVEL_SENSITIVE, .polarity = ACPI_ACTIVE_HIGH, }, }, }; static const struct x86_dev_info chuwi_hi8_info __initconst = { .i2c_client_info = chuwi_hi8_i2c_clients, .i2c_client_count = ARRAY_SIZE(chuwi_hi8_i2c_clients), }; /* * If the EFI bootloader is not Xiaomi's own signed Android loader, then the * Xiaomi Mi Pad 2 X86 tablet sets OSID in the DSDT to 1 (Windows), causing * a bunch of devices to be hidden. * * This takes care of instantiating the hidden devices manually. */ static const char * const bq27520_suppliers[] = { "bq25890-charger" }; static const struct property_entry bq27520_props[] = { PROPERTY_ENTRY_STRING_ARRAY("supplied-from", bq27520_suppliers), { } }; static const struct software_node bq27520_node = { .properties = bq27520_props, }; static const struct x86_i2c_client_info xiaomi_mipad2_i2c_clients[] __initconst = { { /* BQ27520 fuel-gauge */ .board_info = { .type = "bq27520", .addr = 0x55, .dev_name = "bq27520", .swnode = &bq27520_node, }, .adapter_path = "\\_SB_.PCI0.I2C1", }, { /* KTD2026 RGB notification LED controller */ .board_info = { .type = "ktd2026", .addr = 0x30, .dev_name = "ktd2026", }, .adapter_path = "\\_SB_.PCI0.I2C3", }, }; static const struct x86_dev_info xiaomi_mipad2_info __initconst = { .i2c_client_info = xiaomi_mipad2_i2c_clients, .i2c_client_count = ARRAY_SIZE(xiaomi_mipad2_i2c_clients), }; static const struct dmi_system_id x86_android_tablet_ids[] __initconst = { { /* Chuwi Hi8 (CWI509) */ .matches = { DMI_MATCH(DMI_BOARD_VENDOR, "Hampoo"), DMI_MATCH(DMI_BOARD_NAME, "BYT-PA03C"), DMI_MATCH(DMI_SYS_VENDOR, "ilife"), DMI_MATCH(DMI_PRODUCT_NAME, "S806"), }, .driver_data = (void *)&chuwi_hi8_info, }, { /* Xiaomi Mi Pad 2 */ .matches = { DMI_MATCH(DMI_SYS_VENDOR, "Xiaomi Inc"), DMI_MATCH(DMI_PRODUCT_NAME, "Mipad2"), }, .driver_data = (void *)&xiaomi_mipad2_info, }, { } }; MODULE_DEVICE_TABLE(dmi, x86_android_tablet_ids); static int i2c_client_count; static int pdev_count; static int serdev_count; static struct i2c_client **i2c_clients; static struct platform_device **pdevs; static struct serdev_device **serdevs; static struct gpiod_lookup_table **gpiod_lookup_tables; static __init int x86_instantiate_i2c_client(const struct x86_dev_info *dev_info, int idx) { const struct x86_i2c_client_info *client_info = &dev_info->i2c_client_info[idx]; struct i2c_board_info board_info = client_info->board_info; struct i2c_adapter *adap; acpi_handle handle; acpi_status status; board_info.irq = x86_acpi_irq_helper_get(&client_info->irq_data); if (board_info.irq < 0) return board_info.irq; status = acpi_get_handle(NULL, client_info->adapter_path, &handle); if (ACPI_FAILURE(status)) { pr_err("Error could not get %s handle\n", client_info->adapter_path); return -ENODEV; } adap = i2c_acpi_find_adapter_by_handle(handle); if (!adap) { pr_err("error could not get %s adapter\n", client_info->adapter_path); return -ENODEV; } i2c_clients[idx] = i2c_new_client_device(adap, &board_info); put_device(&adap->dev); if (IS_ERR(i2c_clients[idx])) return dev_err_probe(&adap->dev, PTR_ERR(i2c_clients[idx]), "creating I2C-client %d\n", idx); return 0; } static __init int x86_instantiate_serdev(const struct x86_serdev_info *info, int idx) { struct acpi_device *ctrl_adev, *serdev_adev; struct serdev_device *serdev; struct device *ctrl_dev; int ret = -ENODEV; ctrl_adev = acpi_dev_get_first_match_dev(info->ctrl_hid, info->ctrl_uid, -1); if (!ctrl_adev) { pr_err("error could not get %s/%s ctrl adev\n", info->ctrl_hid, info->ctrl_uid); return -ENODEV; } serdev_adev = acpi_dev_get_first_match_dev(info->serdev_hid, NULL, -1); if (!serdev_adev) { pr_err("error could not get %s serdev adev\n", info->serdev_hid); goto put_ctrl_adev; } /* get_first_physical_node() returns a weak ref, no need to put() it */ ctrl_dev = acpi_get_first_physical_node(ctrl_adev); if (!ctrl_dev) { pr_err("error could not get %s/%s ctrl physical dev\n", info->ctrl_hid, info->ctrl_uid); goto put_serdev_adev; } /* ctrl_dev now points to the controller's parent, get the controller */ ctrl_dev = device_find_child_by_name(ctrl_dev, info->ctrl_devname); if (!ctrl_dev) { pr_err("error could not get %s/%s %s ctrl dev\n", info->ctrl_hid, info->ctrl_uid, info->ctrl_devname); goto put_serdev_adev; } serdev = serdev_device_alloc(to_serdev_controller(ctrl_dev)); if (!serdev) { ret = -ENOMEM; goto put_serdev_adev; } ACPI_COMPANION_SET(&serdev->dev, serdev_adev); acpi_device_set_enumerated(serdev_adev); ret = serdev_device_add(serdev); if (ret) { dev_err(&serdev->dev, "error %d adding serdev\n", ret); serdev_device_put(serdev); goto put_serdev_adev; } serdevs[idx] = serdev; put_serdev_adev: acpi_dev_put(serdev_adev); put_ctrl_adev: acpi_dev_put(ctrl_adev); return ret; } static void x86_android_tablet_cleanup(void) { int i; for (i = 0; i < serdev_count; i++) { if (serdevs[i]) serdev_device_remove(serdevs[i]); } kfree(serdevs); for (i = 0; i < pdev_count; i++) platform_device_unregister(pdevs[i]); kfree(pdevs); for (i = 0; i < i2c_client_count; i++) i2c_unregister_device(i2c_clients[i]); kfree(i2c_clients); for (i = 0; gpiod_lookup_tables && gpiod_lookup_tables[i]; i++) gpiod_remove_lookup_table(gpiod_lookup_tables[i]); } static __init int x86_android_tablet_init(void) { const struct x86_dev_info *dev_info; const struct dmi_system_id *id; int i, ret = 0; id = dmi_first_match(x86_android_tablet_ids); if (!id) return -ENODEV; dev_info = id->driver_data; /* * Since this runs from module_init() it cannot use -EPROBE_DEFER, * instead pre-load any modules which are listed as requirements. */ for (i = 0; dev_info->modules && dev_info->modules[i]; i++) request_module(dev_info->modules[i]); gpiod_lookup_tables = dev_info->gpiod_lookup_tables; for (i = 0; gpiod_lookup_tables && gpiod_lookup_tables[i]; i++) gpiod_add_lookup_table(gpiod_lookup_tables[i]); i2c_clients = kcalloc(dev_info->i2c_client_count, sizeof(*i2c_clients), GFP_KERNEL); if (!i2c_clients) return -ENOMEM; i2c_client_count = dev_info->i2c_client_count; for (i = 0; i < i2c_client_count; i++) { ret = x86_instantiate_i2c_client(dev_info, i); if (ret < 0) { x86_android_tablet_cleanup(); return ret; } } pdevs = kcalloc(dev_info->pdev_count, sizeof(*pdevs), GFP_KERNEL); if (!pdevs) { x86_android_tablet_cleanup(); return -ENOMEM; } pdev_count = dev_info->pdev_count; for (i = 0; i < pdev_count; i++) { pdevs[i] = platform_device_register_full(&dev_info->pdev_info[i]); if (IS_ERR(pdevs[i])) { x86_android_tablet_cleanup(); return PTR_ERR(pdevs[i]); } } serdevs = kcalloc(dev_info->serdev_count, sizeof(*serdevs), GFP_KERNEL); if (!serdevs) { x86_android_tablet_cleanup(); return -ENOMEM; } serdev_count = dev_info->serdev_count; for (i = 0; i < serdev_count; i++) { ret = x86_instantiate_serdev(&dev_info->serdev_info[i], i); if (ret < 0) { x86_android_tablet_cleanup(); return ret; } } return 0; } module_init(x86_android_tablet_init); module_exit(x86_android_tablet_cleanup); MODULE_AUTHOR("Hans de Goede