// SPDX-License-Identifier: GPL-2.0-only OR MIT /* * Apple SMC (System Management Controller) MFD driver * * Copyright The Asahi Linux Contributors */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define SMC_ENDPOINT 0x20 /* We don't actually know the true size here but this seem reasonable */ #define SMC_SHMEM_SIZE 0x1000 #define SMC_MAX_SIZE 255 #define SMC_MSG_READ_KEY 0x10 #define SMC_MSG_WRITE_KEY 0x11 #define SMC_MSG_GET_KEY_BY_INDEX 0x12 #define SMC_MSG_GET_KEY_INFO 0x13 #define SMC_MSG_INITIALIZE 0x17 #define SMC_MSG_NOTIFICATION 0x18 #define SMC_MSG_RW_KEY 0x20 #define SMC_DATA GENMASK_ULL(63, 32) #define SMC_WSIZE GENMASK_ULL(31, 24) #define SMC_SIZE GENMASK_ULL(23, 16) #define SMC_ID GENMASK_ULL(15, 12) #define SMC_MSG GENMASK_ULL(7, 0) #define SMC_RESULT SMC_MSG #define SMC_TIMEOUT_MS 500 static const struct mfd_cell apple_smc_devs[] = { MFD_CELL_OF("macsmc-gpio", NULL, NULL, 0, 0, "apple,smc-gpio"), MFD_CELL_OF("macsmc-reboot", NULL, NULL, 0, 0, "apple,smc-reboot"), }; static int apple_smc_cmd_locked(struct apple_smc *smc, u64 cmd, u64 arg, u64 size, u64 wsize, u32 *ret_data) { u8 result; int ret; u64 msg; lockdep_assert_held(&smc->mutex); if (smc->boot_stage != APPLE_SMC_INITIALIZED) return -EIO; if (smc->atomic_mode) return -EIO; reinit_completion(&smc->cmd_done); smc->msg_id = (smc->msg_id + 1) & 0xf; msg = (FIELD_PREP(SMC_MSG, cmd) | FIELD_PREP(SMC_SIZE, size) | FIELD_PREP(SMC_WSIZE, wsize) | FIELD_PREP(SMC_ID, smc->msg_id) | FIELD_PREP(SMC_DATA, arg)); ret = apple_rtkit_send_message(smc->rtk, SMC_ENDPOINT, msg, NULL, false); if (ret) { dev_err(smc->dev, "Failed to send command\n"); return ret; } if (wait_for_completion_timeout(&smc->cmd_done, msecs_to_jiffies(SMC_TIMEOUT_MS)) <= 0) { dev_err(smc->dev, "Command timed out (%llx)", msg); return -ETIMEDOUT; } if (FIELD_GET(SMC_ID, smc->cmd_ret) != smc->msg_id) { dev_err(smc->dev, "Command sequence mismatch (expected %d, got %d)\n", smc->msg_id, (unsigned int)FIELD_GET(SMC_ID, smc->cmd_ret)); return -EIO; } result = FIELD_GET(SMC_RESULT, smc->cmd_ret); if (result) return -EIO; if (ret_data) *ret_data = FIELD_GET(SMC_DATA, smc->cmd_ret); return FIELD_GET(SMC_SIZE, smc->cmd_ret); } static int apple_smc_cmd(struct apple_smc *smc, u64 cmd, u64 arg, u64 size, u64 wsize, u32 *ret_data) { guard(mutex)(&smc->mutex); return apple_smc_cmd_locked(smc, cmd, arg, size, wsize, ret_data); } static int apple_smc_rw_locked(struct apple_smc *smc, smc_key key, const void *wbuf, size_t wsize, void *rbuf, size_t rsize) { u64 smc_size, smc_wsize; u32 rdata; int ret; u64 cmd; lockdep_assert_held(&smc->mutex); if (rsize > SMC_MAX_SIZE) return -EINVAL; if (wsize > SMC_MAX_SIZE) return -EINVAL; if (rsize && wsize) { cmd = SMC_MSG_RW_KEY; memcpy_toio(smc->shmem.iomem, wbuf, wsize); smc_size = rsize; smc_wsize = wsize; } else if (wsize && !rsize) { cmd = SMC_MSG_WRITE_KEY; memcpy_toio(smc->shmem.iomem, wbuf, wsize); /* * Setting size to the length we want to write and wsize to 0 * looks silly but that's how the SMC protocol works ¯\_(ツ)_/¯ */ smc_size = wsize; smc_wsize = 0; } else if (!wsize && rsize) { cmd = SMC_MSG_READ_KEY; smc_size = rsize; smc_wsize = 0; } else { return -EINVAL; } ret = apple_smc_cmd_locked(smc, cmd, key, smc_size, smc_wsize, &rdata); if (ret < 0) return ret; if (rsize) { /* * Small data <= 4 bytes is returned as part of the reply * message which is sent over the mailbox FIFO. Everything * bigger has to be copied from SRAM which is mapped as * Device memory. */ if (rsize <= 4) memcpy(rbuf, &rdata, rsize); else memcpy_fromio(rbuf, smc->shmem.iomem, rsize); } return ret; } int apple_smc_read(struct apple_smc *smc, smc_key key, void *buf, size_t size) { guard(mutex)(&smc->mutex); return apple_smc_rw_locked(smc, key, NULL, 0, buf, size); } EXPORT_SYMBOL(apple_smc_read); int apple_smc_write(struct apple_smc *smc, smc_key key, void *buf, size_t size) { guard(mutex)(&smc->mutex); return apple_smc_rw_locked(smc, key, buf, size, NULL, 0); } EXPORT_SYMBOL(apple_smc_write); int apple_smc_rw(struct apple_smc *smc, smc_key key, void *wbuf, size_t wsize, void *rbuf, size_t rsize) { guard(mutex)(&smc->mutex); return apple_smc_rw_locked(smc, key, wbuf, wsize, rbuf, rsize); } EXPORT_SYMBOL(apple_smc_rw); int apple_smc_get_key_by_index(struct apple_smc *smc, int index, smc_key *key) { int ret; ret = apple_smc_cmd(smc, SMC_MSG_GET_KEY_BY_INDEX, index, 0, 0, key); *key = swab32(*key); return ret; } EXPORT_SYMBOL(apple_smc_get_key_by_index); int apple_smc_get_key_info(struct apple_smc *smc, smc_key key, struct apple_smc_key_info *info) { u8 key_info[6]; int ret; ret = apple_smc_cmd(smc, SMC_MSG_GET_KEY_INFO, key, 0, 0, NULL); if (ret >= 0 && info) { memcpy_fromio(key_info, smc->shmem.iomem, sizeof(key_info)); info->size = key_info[0]; info->type_code = get_unaligned_be32(&key_info[1]); info->flags = key_info[5]; } return ret; } EXPORT_SYMBOL(apple_smc_get_key_info); int apple_smc_enter_atomic(struct apple_smc *smc) { guard(mutex)(&smc->mutex); /* * Disable notifications since this is called before shutdown and no * notification handler will be able to handle the notification * using atomic operations only. Also ignore any failure here * because we're about to shut down or reboot anyway. * We can't use apple_smc_write_flag here since that would try to lock * smc->mutex again. */ const u8 flag = 0; apple_smc_rw_locked(smc, SMC_KEY(NTAP), &flag, sizeof(flag), NULL, 0); smc->atomic_mode = true; return 0; } EXPORT_SYMBOL(apple_smc_enter_atomic); int apple_smc_write_atomic(struct apple_smc *smc, smc_key key, void *buf, size_t size) { guard(spinlock_irqsave)(&smc->lock); u8 result; int ret; u64 msg; if (size > SMC_MAX_SIZE || size == 0) return -EINVAL; if (smc->boot_stage != APPLE_SMC_INITIALIZED) return -EIO; if (!smc->atomic_mode) return -EIO; memcpy_toio(smc->shmem.iomem, buf, size); smc->msg_id = (smc->msg_id + 1) & 0xf; msg = (FIELD_PREP(SMC_MSG, SMC_MSG_WRITE_KEY) | FIELD_PREP(SMC_SIZE, size) | FIELD_PREP(SMC_ID, smc->msg_id) | FIELD_PREP(SMC_DATA, key)); smc->atomic_pending = true; ret = apple_rtkit_send_message(smc->rtk, SMC_ENDPOINT, msg, NULL, true); if (ret < 0) { dev_err(smc->dev, "Failed to send command (%d)\n", ret); return ret; } while (smc->atomic_pending) { ret = apple_rtkit_poll(smc->rtk); if (ret < 0) { dev_err(smc->dev, "RTKit poll failed (%llx)", msg); return ret; } udelay(100); } if (FIELD_GET(SMC_ID, smc->cmd_ret) != smc->msg_id) { dev_err(smc->dev, "Command sequence mismatch (expected %d, got %d)\n", smc->msg_id, (unsigned int)FIELD_GET(SMC_ID, smc->cmd_ret)); return -EIO; } result = FIELD_GET(SMC_RESULT, smc->cmd_ret); if (result) return -EIO; return FIELD_GET(SMC_SIZE, smc->cmd_ret); } EXPORT_SYMBOL(apple_smc_write_atomic); static void apple_smc_rtkit_crashed(void *cookie, const void *bfr, size_t bfr_len) { struct apple_smc *smc = cookie; smc->boot_stage = APPLE_SMC_ERROR_CRASHED; dev_err(smc->dev, "SMC crashed! Your system will reboot in a few seconds...\n"); } static int apple_smc_rtkit_shmem_setup(void *cookie, struct apple_rtkit_shmem *bfr) { struct apple_smc *smc = cookie; size_t bfr_end; if (!bfr->iova) { dev_err(smc->dev, "RTKit wants a RAM buffer\n"); return -EIO; } if (check_add_overflow(bfr->iova, bfr->size - 1, &bfr_end)) return -EFAULT; if (bfr->iova < smc->sram->start || bfr->iova > smc->sram->end || bfr_end > smc->sram->end) { dev_err(smc->dev, "RTKit buffer request outside SRAM region: [0x%llx, 0x%llx]\n", (unsigned long long)bfr->iova, (unsigned long long)bfr_end); return -EFAULT; } bfr->iomem = smc->sram_base + (bfr->iova - smc->sram->start); bfr->is_mapped = true; return 0; } static bool apple_smc_rtkit_recv_early(void *cookie, u8 endpoint, u64 message) { struct apple_smc *smc = cookie; if (endpoint != SMC_ENDPOINT) { dev_warn(smc->dev, "Received message for unknown endpoint 0x%x\n", endpoint); return false; } if (smc->boot_stage == APPLE_SMC_BOOTING) { int ret; smc->shmem.iova = message; smc->shmem.size = SMC_SHMEM_SIZE; ret = apple_smc_rtkit_shmem_setup(smc, &smc->shmem); if (ret < 0) { smc->boot_stage = APPLE_SMC_ERROR_NO_SHMEM; dev_err(smc->dev, "Failed to initialize shared memory (%d)\n", ret); } else { smc->boot_stage = APPLE_SMC_INITIALIZED; } complete(&smc->init_done); } else if (FIELD_GET(SMC_MSG, message) == SMC_MSG_NOTIFICATION) { /* Handle these in the RTKit worker thread */ return false; } else { smc->cmd_ret = message; if (smc->atomic_pending) smc->atomic_pending = false; else complete(&smc->cmd_done); } return true; } static void apple_smc_rtkit_recv(void *cookie, u8 endpoint, u64 message) { struct apple_smc *smc = cookie; if (endpoint != SMC_ENDPOINT) { dev_warn(smc->dev, "Received message for unknown endpoint 0x%x\n", endpoint); return; } if (FIELD_GET(SMC_MSG, message) != SMC_MSG_NOTIFICATION) { dev_warn(smc->dev, "Received unknown message from worker: 0x%llx\n", message); return; } blocking_notifier_call_chain(&smc->event_handlers, FIELD_GET(SMC_DATA, message), NULL); } static const struct apple_rtkit_ops apple_smc_rtkit_ops = { .crashed = apple_smc_rtkit_crashed, .recv_message = apple_smc_rtkit_recv, .recv_message_early = apple_smc_rtkit_recv_early, .shmem_setup = apple_smc_rtkit_shmem_setup, }; static void apple_smc_rtkit_shutdown(void *data) { struct apple_smc *smc = data; /* Shut down SMC firmware, if it's not completely wedged */ if (apple_rtkit_is_running(smc->rtk)) apple_rtkit_quiesce(smc->rtk); } static void apple_smc_disable_notifications(void *data) { struct apple_smc *smc = data; apple_smc_write_flag(smc, SMC_KEY(NTAP), false); } static int apple_smc_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct apple_smc *smc; u32 count; int ret; smc = devm_kzalloc(dev, sizeof(*smc), GFP_KERNEL); if (!smc) return -ENOMEM; smc->dev = &pdev->dev; smc->sram_base = devm_platform_get_and_ioremap_resource(pdev, 1, &smc->sram); if (IS_ERR(smc->sram_base)) return dev_err_probe(dev, PTR_ERR(smc->sram_base), "Failed to map SRAM region"); smc->rtk = devm_apple_rtkit_init(dev, smc, NULL, 0, &apple_smc_rtkit_ops); if (IS_ERR(smc->rtk)) return dev_err_probe(dev, PTR_ERR(smc->rtk), "Failed to initialize RTKit"); smc->boot_stage = APPLE_SMC_BOOTING; ret = apple_rtkit_wake(smc->rtk); if (ret) return dev_err_probe(dev, ret, "Failed to wake up SMC"); ret = devm_add_action_or_reset(dev, apple_smc_rtkit_shutdown, smc); if (ret) return dev_err_probe(dev, ret, "Failed to register rtkit shutdown action"); ret = apple_rtkit_start_ep(smc->rtk, SMC_ENDPOINT); if (ret) return dev_err_probe(dev, ret, "Failed to start SMC endpoint"); init_completion(&smc->init_done); init_completion(&smc->cmd_done); ret = apple_rtkit_send_message(smc->rtk, SMC_ENDPOINT, FIELD_PREP(SMC_MSG, SMC_MSG_INITIALIZE), NULL, false); if (ret) return dev_err_probe(dev, ret, "Failed to send init message"); if (wait_for_completion_timeout(&smc->init_done, msecs_to_jiffies(SMC_TIMEOUT_MS)) == 0) { dev_err(dev, "Timed out initializing SMC"); return -ETIMEDOUT; } if (smc->boot_stage != APPLE_SMC_INITIALIZED) { dev_err(dev, "SMC failed to boot successfully, boot stage=%d\n", smc->boot_stage); return -EIO; } dev_set_drvdata(&pdev->dev, smc); BLOCKING_INIT_NOTIFIER_HEAD(&smc->event_handlers); ret = apple_smc_read_u32(smc, SMC_KEY(#KEY), &count); if (ret) return dev_err_probe(smc->dev, ret, "Failed to get key count"); smc->key_count = be32_to_cpu(count); /* Enable notifications */ apple_smc_write_flag(smc, SMC_KEY(NTAP), true); ret = devm_add_action_or_reset(dev, apple_smc_disable_notifications, smc); if (ret) return dev_err_probe(dev, ret, "Failed to register notification disable action"); ret = devm_mfd_add_devices(smc->dev, PLATFORM_DEVID_NONE, apple_smc_devs, ARRAY_SIZE(apple_smc_devs), NULL, 0, NULL); if (ret) return dev_err_probe(smc->dev, ret, "Failed to register sub-devices"); return 0; } static const struct of_device_id apple_smc_of_match[] = { { .compatible = "apple,smc" }, {}, }; MODULE_DEVICE_TABLE(of, apple_smc_of_match); static struct platform_driver apple_smc_driver = { .driver = { .name = "macsmc", .of_match_table = apple_smc_of_match, }, .probe = apple_smc_probe, }; module_platform_driver(apple_smc_driver); MODULE_AUTHOR("Hector Martin "); MODULE_AUTHOR("Sven Peter "); MODULE_LICENSE("Dual MIT/GPL"); MODULE_DESCRIPTION("Apple SMC driver");