mirror of
git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2025-08-05 16:54:27 +00:00

The System Management Controller (SMC) on Apple Silicon machines is a piece of hardware that exposes various functionalities such as temperature sensors, voltage/power meters, shutdown/reboot handling, GPIOs and more. Communication happens via a shared mailbox using the RTKit protocol which is also used for other co-processors. The SMC protocol then allows reading and writing many different keys which implement the various features. The MFD core device handles this protocol and exposes it to the sub-devices. Some of the sub-devices are potentially also useful on pre-M1 Apple machines and support for SMCs on these machines can be added at a later time. Co-developed-by: Hector Martin <marcan@marcan.st> Signed-off-by: Hector Martin <marcan@marcan.st> Reviewed-by: Alyssa Rosenzweig <alyssa@rosenzweig.io> Reviewed-by: Neal Gompa <neal@gompa.dev> Signed-off-by: Sven Peter <sven@kernel.org> Link: https://lore.kernel.org/r/20250610-smc-6-15-v7-5-556cafd771d3@kernel.org Signed-off-by: Lee Jones <lee@kernel.org>
498 lines
13 KiB
C
498 lines
13 KiB
C
// SPDX-License-Identifier: GPL-2.0-only OR MIT
|
|
/*
|
|
* Apple SMC (System Management Controller) MFD driver
|
|
*
|
|
* Copyright The Asahi Linux Contributors
|
|
*/
|
|
|
|
#include <linux/bitfield.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/device.h>
|
|
#include <linux/io.h>
|
|
#include <linux/ioport.h>
|
|
#include <linux/math.h>
|
|
#include <linux/mfd/core.h>
|
|
#include <linux/mfd/macsmc.h>
|
|
#include <linux/notifier.h>
|
|
#include <linux/of.h>
|
|
#include <linux/of_platform.h>
|
|
#include <linux/overflow.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/soc/apple/rtkit.h>
|
|
#include <linux/unaligned.h>
|
|
|
|
#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 <marcan@marcan.st>");
|
|
MODULE_AUTHOR("Sven Peter <sven@kernel.org>");
|
|
MODULE_LICENSE("Dual MIT/GPL");
|
|
MODULE_DESCRIPTION("Apple SMC driver");
|