/* * Copyright (c) 2012-2013, The Linux Foundation. All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 and * only version 2 as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "peripheral-loader.h" #include "pil-q6v5.h" #include "scm-pas.h" #include "sysmon.h" #define QDSP6SS_RST_EVB 0x010 #define PROXY_TIMEOUT_MS 10000 static struct kobject *lpass_status; static char status[32]; struct lpass_data { struct q6v5_data *q6; struct subsys_device *subsys; struct subsys_desc subsys_desc; void *ramdump_dev; int wdog_irq; struct work_struct work; void *wcnss_notif_hdle; void *modem_notif_hdle; int crash_shutdown; unsigned int err_fatal_irq; int force_stop_gpio; }; #define subsys_to_drv(d) container_of(d, struct lpass_data, subsys_desc) static int pil_lpass_enable_clks(struct q6v5_data *drv) { int ret; ret = clk_reset(drv->core_clk, CLK_RESET_DEASSERT); if (ret) goto err_reset; ret = clk_prepare_enable(drv->core_clk); if (ret) goto err_core_clk; ret = clk_prepare_enable(drv->ahb_clk); if (ret) goto err_ahb_clk; ret = clk_prepare_enable(drv->axi_clk); if (ret) goto err_axi_clk; ret = clk_prepare_enable(drv->reg_clk); if (ret) goto err_reg_clk; return 0; err_reg_clk: clk_disable_unprepare(drv->axi_clk); err_axi_clk: clk_disable_unprepare(drv->ahb_clk); err_ahb_clk: clk_disable_unprepare(drv->core_clk); err_core_clk: clk_reset(drv->core_clk, CLK_RESET_ASSERT); err_reset: return ret; } static void pil_lpass_disable_clks(struct q6v5_data *drv) { clk_disable_unprepare(drv->reg_clk); clk_disable_unprepare(drv->axi_clk); clk_disable_unprepare(drv->ahb_clk); clk_disable_unprepare(drv->core_clk); clk_reset(drv->core_clk, CLK_RESET_ASSERT); } static int pil_lpass_shutdown(struct pil_desc *pil) { struct q6v5_data *drv = container_of(pil, struct q6v5_data, desc); pil_q6v5_halt_axi_port(pil, drv->axi_halt_base); /* * If the shutdown function is called before the reset function, clocks * will not be enabled yet. Enable them here so that register writes * performed during the shutdown succeed. */ if (drv->is_booted == false) pil_lpass_enable_clks(drv); pil_q6v5_shutdown(pil); pil_lpass_disable_clks(drv); writel_relaxed(1, drv->restart_reg); drv->is_booted = false; return 0; } static int pil_lpass_reset(struct pil_desc *pil) { struct q6v5_data *drv = container_of(pil, struct q6v5_data, desc); phys_addr_t start_addr = pil_get_entry_addr(pil); int ret; /* Deassert reset to subsystem and wait for propagation */ writel_relaxed(0, drv->restart_reg); mb(); udelay(2); ret = pil_lpass_enable_clks(drv); if (ret) return ret; /* Program Image Address */ writel_relaxed((start_addr >> 4) & 0x0FFFFFF0, drv->reg_base + QDSP6SS_RST_EVB); ret = pil_q6v5_reset(pil); if (ret) { pil_lpass_disable_clks(drv); return ret; } drv->is_booted = true; return 0; } static struct pil_reset_ops pil_lpass_ops = { .proxy_vote = pil_q6v5_make_proxy_votes, .proxy_unvote = pil_q6v5_remove_proxy_votes, .auth_and_reset = pil_lpass_reset, .shutdown = pil_lpass_shutdown, }; static int pil_lpass_init_image_trusted(struct pil_desc *pil, const u8 *metadata, size_t size) { return pas_init_image(PAS_Q6, metadata, size); } static int pil_lpass_mem_setup_trusted(struct pil_desc *pil, phys_addr_t addr, size_t size) { return pas_mem_setup(PAS_Q6, addr, size); } static int pil_lpass_reset_trusted(struct pil_desc *pil) { struct q6v5_data *drv = container_of(pil, struct q6v5_data, desc); int ret; ret = clk_prepare_enable(drv->axi_clk); if (ret) return ret; return pas_auth_and_reset(PAS_Q6); } static int pil_lpass_shutdown_trusted(struct pil_desc *pil) { struct q6v5_data *drv = container_of(pil, struct q6v5_data, desc); int ret; ret = pas_shutdown(PAS_Q6); if (ret) return ret; clk_disable_unprepare(drv->axi_clk); return 0; } static struct pil_reset_ops pil_lpass_ops_trusted = { .init_image = pil_lpass_init_image_trusted, .mem_setup = pil_lpass_mem_setup_trusted, .proxy_vote = pil_q6v5_make_proxy_votes, .proxy_unvote = pil_q6v5_remove_proxy_votes, .auth_and_reset = pil_lpass_reset_trusted, .shutdown = pil_lpass_shutdown_trusted, }; static int wcnss_notifier_cb(struct notifier_block *this, unsigned long code, void *ss_handle) { int ret; pr_debug("%s: W-Notify: event %lu\n", __func__, code); ret = sysmon_send_event(SYSMON_SS_LPASS, "wcnss", code); if (ret < 0) pr_err("%s: sysmon_send_event error %d", __func__, ret); return NOTIFY_DONE; } static struct notifier_block wnb = { .notifier_call = wcnss_notifier_cb, }; static int modem_notifier_cb(struct notifier_block *this, unsigned long code, void *ss_handle) { int ret; pr_debug("%s: M-Notify: event %lu\n", __func__, code); ret = sysmon_send_event(SYSMON_SS_LPASS, "modem", code); if (ret < 0) pr_err("%s: sysmon_send_event error %d", __func__, ret); return NOTIFY_DONE; } static struct notifier_block mnb = { .notifier_call = modem_notifier_cb, }; static void adsp_log_failure_reason(void) { char *reason; char buffer[81]; unsigned size; reason = smem_get_entry(SMEM_SSR_REASON_LPASS0, &size); if (!reason) { pr_err("ADSP subsystem failure reason: (unknown, smem_get_entry failed)."); return; } if (reason[0] == '\0') { pr_err("ADSP subsystem failure reason: (unknown, init value found)"); return; } size = min(size, sizeof(buffer) - 1); memcpy(buffer, reason, size); buffer[size] = '\0'; pr_err("ADSP subsystem failure reason: %s", buffer); memset((void *)reason, 0x0, size); wmb(); } static void restart_adsp(struct lpass_data *drv) { adsp_log_failure_reason(); subsystem_restart_dev(drv->subsys); } static void adsp_fatal_fn(struct work_struct *work) { struct lpass_data *drv = container_of(work, struct lpass_data, work); pr_err("Watchdog bite received from ADSP!\n"); restart_adsp(drv); } static irqreturn_t adsp_err_fatal_intr_handler (int irq, void *dev_id) { struct lpass_data *drv = dev_id; /* Ignore if we're the one that set the force stop bit in the outbound * entry */ if (drv->crash_shutdown) return IRQ_HANDLED; pr_err("Fatal error on the ADSP!\n"); restart_adsp(drv); return IRQ_HANDLED; } #define SCM_Q6_NMI_CMD 0x1 static void send_q6_nmi(void) { /* Send NMI to QDSP6 via an SCM call. */ scm_call_atomic1(SCM_SVC_UTIL, SCM_Q6_NMI_CMD, 0x1); pr_debug("%s: Q6 NMI was sent.\n", __func__); } /* * The "status" file where a static variable is read from and written to. */ static ssize_t adsp_state_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) { return snprintf(buf, sizeof(status), "%s\n", status); } static struct kobj_attribute adsp_state_attribute = __ATTR(status, 0444, adsp_state_show, NULL); static struct attribute *attrs[] = { &adsp_state_attribute.attr, NULL, /* need to NULL terminate the list of attributes */ }; static struct attribute_group attr_group = { .attrs = attrs, }; static void adsp_set_state(char *state) { strlcpy(status, state, sizeof(status)); sysfs_notify(lpass_status, NULL, "status"); } #define subsys_to_lpass(d) container_of(d, struct lpass_data, subsys_desc) static int adsp_shutdown(const struct subsys_desc *subsys) { struct lpass_data *drv = subsys_to_lpass(subsys); send_q6_nmi(); /* The write needs to go through before the q6 is shutdown. */ mb(); pil_shutdown(&drv->q6->desc); disable_irq_nosync(drv->wdog_irq); pr_debug("ADSP is Down\n"); adsp_set_state("OFFLINE"); return 0; } static int adsp_powerup(const struct subsys_desc *subsys) { struct lpass_data *drv = subsys_to_lpass(subsys); int ret = 0; ret = pil_boot(&drv->q6->desc); enable_irq(drv->wdog_irq); pr_debug("ADSP is back online\n"); adsp_set_state("ONLINE"); return ret; } static int adsp_ramdump(int enable, const struct subsys_desc *subsys) { struct lpass_data *drv = subsys_to_lpass(subsys); if (!enable) return 0; return pil_do_ramdump(&drv->q6->desc, drv->ramdump_dev); } static void adsp_crash_shutdown(const struct subsys_desc *subsys) { struct lpass_data *drv = subsys_to_lpass(subsys); drv->crash_shutdown = 1; gpio_set_value(drv->force_stop_gpio, 1); send_q6_nmi(); } static irqreturn_t adsp_wdog_bite_irq(int irq, void *dev_id) { struct lpass_data *drv = dev_id; schedule_work(&drv->work); return IRQ_HANDLED; } static int lpass_start(const struct subsys_desc *desc) { struct lpass_data *drv = subsys_to_drv(desc); return pil_boot(&drv->q6->desc); } static void lpass_stop(const struct subsys_desc *desc) { struct lpass_data *drv = subsys_to_drv(desc); pil_shutdown(&drv->q6->desc); } static int __devinit pil_lpass_driver_probe(struct platform_device *pdev) { struct lpass_data *drv; struct q6v5_data *q6; struct pil_desc *desc; struct resource *res; int ret, gpio_clk_ready; drv = devm_kzalloc(&pdev->dev, sizeof(*drv), GFP_KERNEL); if (!drv) return -ENOMEM; platform_set_drvdata(pdev, drv); drv->wdog_irq = platform_get_irq(pdev, 0); if (drv->wdog_irq < 0) return drv->wdog_irq; ret = gpio_to_irq(of_get_named_gpio(pdev->dev.of_node, "qcom,gpio-err-fatal", 0)); if (ret < 0) return ret; drv->err_fatal_irq = ret; ret = gpio_to_irq(of_get_named_gpio(pdev->dev.of_node, "qcom,gpio-proxy-unvote", 0)); if (ret < 0) return ret; gpio_clk_ready = ret; drv->force_stop_gpio = of_get_named_gpio(pdev->dev.of_node, "qcom,gpio-force-stop", 0); if (drv->force_stop_gpio < 0) return drv->force_stop_gpio; q6 = pil_q6v5_init(pdev); if (IS_ERR(q6)) return PTR_ERR(q6); drv->q6 = q6; desc = &q6->desc; desc->owner = THIS_MODULE; desc->proxy_timeout = PROXY_TIMEOUT_MS; desc->proxy_unvote_irq = gpio_clk_ready; res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "restart_reg"); q6->restart_reg = devm_request_and_ioremap(&pdev->dev, res); if (!q6->restart_reg) return -ENOMEM; q6->core_clk = devm_clk_get(&pdev->dev, "core_clk"); if (IS_ERR(q6->core_clk)) return PTR_ERR(q6->core_clk); q6->ahb_clk = devm_clk_get(&pdev->dev, "iface_clk"); if (IS_ERR(q6->ahb_clk)) return PTR_ERR(q6->ahb_clk); q6->axi_clk = devm_clk_get(&pdev->dev, "bus_clk"); if (IS_ERR(q6->axi_clk)) return PTR_ERR(q6->axi_clk); q6->reg_clk = devm_clk_get(&pdev->dev, "reg_clk"); if (IS_ERR(q6->reg_clk)) return PTR_ERR(q6->reg_clk); if (pas_supported(PAS_Q6) > 0) { desc->ops = &pil_lpass_ops_trusted; dev_info(&pdev->dev, "using secure boot\n"); } else { desc->ops = &pil_lpass_ops; dev_info(&pdev->dev, "using non-secure boot\n"); } ret = pil_desc_init(desc); if (ret) return ret; drv->subsys_desc.name = desc->name; drv->subsys_desc.owner = THIS_MODULE; drv->subsys_desc.dev = &pdev->dev; drv->subsys_desc.shutdown = adsp_shutdown; drv->subsys_desc.powerup = adsp_powerup; drv->subsys_desc.ramdump = adsp_ramdump; drv->subsys_desc.crash_shutdown = adsp_crash_shutdown; drv->subsys_desc.start = lpass_start; drv->subsys_desc.stop = lpass_stop; INIT_WORK(&drv->work, adsp_fatal_fn); drv->ramdump_dev = create_ramdump_device("adsp", &pdev->dev); if (!drv->ramdump_dev) { ret = -ENOMEM; goto err_ramdump; } drv->subsys = subsys_register(&drv->subsys_desc); if (IS_ERR(drv->subsys)) { ret = PTR_ERR(drv->subsys); goto err_subsys; } ret = devm_request_irq(&pdev->dev, drv->wdog_irq, adsp_wdog_bite_irq, IRQF_TRIGGER_RISING, dev_name(&pdev->dev), drv); if (ret) goto err_irq; ret = devm_request_irq(&pdev->dev, drv->err_fatal_irq, adsp_err_fatal_intr_handler, IRQF_TRIGGER_RISING, dev_name(&pdev->dev), drv); if (ret) goto err_irq; drv->wcnss_notif_hdle = subsys_notif_register_notifier("wcnss", &wnb); if (IS_ERR(drv->wcnss_notif_hdle)) { ret = PTR_ERR(drv->wcnss_notif_hdle); goto err_notif_wcnss; } drv->modem_notif_hdle = subsys_notif_register_notifier("modem", &mnb); if (IS_ERR(drv->modem_notif_hdle)) { ret = PTR_ERR(drv->modem_notif_hdle); goto err_notif_modem; } lpass_status = kobject_create_and_add("audio_voice_service", kernel_kobj); if (!lpass_status) { pr_err("%s: kobject create failed\n", __func__); ret = -ENOMEM; goto err_notif_modem; } ret = sysfs_create_group(lpass_status, &attr_group); if (ret) { pr_err("%s: sysfs create group failed\n", __func__); goto err_kobj; } adsp_set_state("ONLINE"); return 0; err_kobj: kobject_put(lpass_status); err_notif_modem: subsys_notif_unregister_notifier(drv->wcnss_notif_hdle, &wnb); err_notif_wcnss: err_irq: subsys_unregister(drv->subsys); err_subsys: destroy_ramdump_device(drv->ramdump_dev); err_ramdump: pil_desc_release(desc); return ret; } static int __devexit pil_lpass_driver_exit(struct platform_device *pdev) { struct lpass_data *drv = platform_get_drvdata(pdev); subsys_notif_unregister_notifier(drv->wcnss_notif_hdle, &wnb); subsys_notif_unregister_notifier(drv->modem_notif_hdle, &mnb); subsys_unregister(drv->subsys); destroy_ramdump_device(drv->ramdump_dev); pil_desc_release(&drv->q6->desc); sysfs_remove_group(lpass_status, &attr_group); kobject_del(lpass_status); return 0; } static struct of_device_id lpass_match_table[] = { { .compatible = "qcom,pil-q6v5-lpass" }, {} }; static struct platform_driver pil_lpass_driver = { .probe = pil_lpass_driver_probe, .remove = __devexit_p(pil_lpass_driver_exit), .driver = { .name = "pil-q6v5-lpass", .of_match_table = lpass_match_table, .owner = THIS_MODULE, }, }; static int __init pil_lpass_init(void) { return platform_driver_register(&pil_lpass_driver); } module_init(pil_lpass_init); static void __exit pil_lpass_exit(void) { platform_driver_unregister(&pil_lpass_driver); } module_exit(pil_lpass_exit); MODULE_DESCRIPTION("Support for booting low-power audio subsystems with QDSP6v5 (Hexagon) processors"); MODULE_LICENSE("GPL v2");