mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/chenhuacai/linux-loongson
synced 2025-08-28 00:19:36 +00:00

This driver implements the reboot/shutdown support exposed by the SMC on Apple Silicon machines, such as Apple M1 Macs. Signed-off-by: Hector Martin <marcan@marcan.st> Reviewed-by: Alyssa Rosenzweig <alyssa@rosenzweig.io> Reviewed-by: Neal Gompa <neal@gompa.dev> Reviewed-by: Sebastian Reichel <sebastian.reichel@collabora.com> Signed-off-by: Sven Peter <sven@kernel.org> Link: https://lore.kernel.org/r/20250610-smc-6-15-v7-7-556cafd771d3@kernel.org Signed-off-by: Lee Jones <lee@kernel.org>
291 lines
7.8 KiB
C
291 lines
7.8 KiB
C
// SPDX-License-Identifier: GPL-2.0-only OR MIT
|
|
/*
|
|
* Apple SMC Reboot/Poweroff Handler
|
|
* Copyright The Asahi Linux Contributors
|
|
*/
|
|
|
|
#include <linux/delay.h>
|
|
#include <linux/mfd/core.h>
|
|
#include <linux/mfd/macsmc.h>
|
|
#include <linux/mod_devicetable.h>
|
|
#include <linux/module.h>
|
|
#include <linux/nvmem-consumer.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/reboot.h>
|
|
#include <linux/slab.h>
|
|
|
|
struct macsmc_reboot_nvmem {
|
|
struct nvmem_cell *shutdown_flag;
|
|
struct nvmem_cell *boot_stage;
|
|
struct nvmem_cell *boot_error_count;
|
|
struct nvmem_cell *panic_count;
|
|
};
|
|
|
|
static const char * const nvmem_names[] = {
|
|
"shutdown_flag",
|
|
"boot_stage",
|
|
"boot_error_count",
|
|
"panic_count",
|
|
};
|
|
|
|
enum boot_stage {
|
|
BOOT_STAGE_SHUTDOWN = 0x00, /* Clean shutdown */
|
|
BOOT_STAGE_IBOOT_DONE = 0x2f, /* Last stage of bootloader */
|
|
BOOT_STAGE_KERNEL_STARTED = 0x30, /* Normal OS booting */
|
|
};
|
|
|
|
struct macsmc_reboot {
|
|
struct device *dev;
|
|
struct apple_smc *smc;
|
|
struct notifier_block reboot_notify;
|
|
|
|
union {
|
|
struct macsmc_reboot_nvmem nvm;
|
|
struct nvmem_cell *nvm_cells[ARRAY_SIZE(nvmem_names)];
|
|
};
|
|
};
|
|
|
|
/* Helpers to read/write a u8 given a struct nvmem_cell */
|
|
static int nvmem_cell_get_u8(struct nvmem_cell *cell)
|
|
{
|
|
size_t len;
|
|
void *bfr;
|
|
u8 val;
|
|
|
|
bfr = nvmem_cell_read(cell, &len);
|
|
if (IS_ERR(bfr))
|
|
return PTR_ERR(bfr);
|
|
|
|
if (len < 1) {
|
|
kfree(bfr);
|
|
return -EINVAL;
|
|
}
|
|
|
|
val = *(u8 *)bfr;
|
|
kfree(bfr);
|
|
return val;
|
|
}
|
|
|
|
static int nvmem_cell_set_u8(struct nvmem_cell *cell, u8 val)
|
|
{
|
|
return nvmem_cell_write(cell, &val, sizeof(val));
|
|
}
|
|
|
|
/*
|
|
* SMC 'MBSE' key actions:
|
|
*
|
|
* 'offw' - shutdown warning
|
|
* 'slpw' - sleep warning
|
|
* 'rest' - restart warning
|
|
* 'off1' - shutdown (needs PMU bit set to stay on)
|
|
* 'susp' - suspend
|
|
* 'phra' - restart ("PE Halt Restart Action"?)
|
|
* 'panb' - panic beginning
|
|
* 'pane' - panic end
|
|
*/
|
|
|
|
static int macsmc_prepare_atomic(struct sys_off_data *data)
|
|
{
|
|
struct macsmc_reboot *reboot = data->cb_data;
|
|
|
|
dev_info(reboot->dev, "Preparing SMC for atomic mode\n");
|
|
|
|
apple_smc_enter_atomic(reboot->smc);
|
|
return NOTIFY_OK;
|
|
}
|
|
|
|
static int macsmc_power_off(struct sys_off_data *data)
|
|
{
|
|
struct macsmc_reboot *reboot = data->cb_data;
|
|
|
|
dev_info(reboot->dev, "Issuing power off (off1)\n");
|
|
|
|
if (apple_smc_write_u32_atomic(reboot->smc, SMC_KEY(MBSE), SMC_KEY(off1)) < 0) {
|
|
dev_err(reboot->dev, "Failed to issue MBSE = off1 (power_off)\n");
|
|
} else {
|
|
mdelay(100);
|
|
WARN_ONCE(1, "Unable to power off system\n");
|
|
}
|
|
|
|
return NOTIFY_OK;
|
|
}
|
|
|
|
static int macsmc_restart(struct sys_off_data *data)
|
|
{
|
|
struct macsmc_reboot *reboot = data->cb_data;
|
|
|
|
dev_info(reboot->dev, "Issuing restart (phra)\n");
|
|
|
|
if (apple_smc_write_u32_atomic(reboot->smc, SMC_KEY(MBSE), SMC_KEY(phra)) < 0) {
|
|
dev_err(reboot->dev, "Failed to issue MBSE = phra (restart)\n");
|
|
} else {
|
|
mdelay(100);
|
|
WARN_ONCE(1, "Unable to restart system\n");
|
|
}
|
|
|
|
return NOTIFY_OK;
|
|
}
|
|
|
|
static int macsmc_reboot_notify(struct notifier_block *this, unsigned long action, void *data)
|
|
{
|
|
struct macsmc_reboot *reboot = container_of(this, struct macsmc_reboot, reboot_notify);
|
|
u8 shutdown_flag;
|
|
u32 val;
|
|
|
|
switch (action) {
|
|
case SYS_RESTART:
|
|
val = SMC_KEY(rest);
|
|
shutdown_flag = 0;
|
|
break;
|
|
case SYS_POWER_OFF:
|
|
val = SMC_KEY(offw);
|
|
shutdown_flag = 1;
|
|
break;
|
|
default:
|
|
return NOTIFY_DONE;
|
|
}
|
|
|
|
dev_info(reboot->dev, "Preparing for reboot (%p4ch)\n", &val);
|
|
|
|
/* On the Mac Mini, this will turn off the LED for power off */
|
|
if (apple_smc_write_u32(reboot->smc, SMC_KEY(MBSE), val) < 0)
|
|
dev_err(reboot->dev, "Failed to issue MBSE = %p4ch (reboot_prepare)\n", &val);
|
|
|
|
/* Set the boot_stage to 0, which means we're doing a clean shutdown/reboot. */
|
|
if (reboot->nvm.boot_stage &&
|
|
nvmem_cell_set_u8(reboot->nvm.boot_stage, BOOT_STAGE_SHUTDOWN) < 0)
|
|
dev_err(reboot->dev, "Failed to write boot_stage\n");
|
|
|
|
/*
|
|
* Set the PMU flag to actually reboot into the off state.
|
|
* Without this, the device will just reboot. We make it optional in case it is no longer
|
|
* necessary on newer hardware.
|
|
*/
|
|
if (reboot->nvm.shutdown_flag &&
|
|
nvmem_cell_set_u8(reboot->nvm.shutdown_flag, shutdown_flag) < 0)
|
|
dev_err(reboot->dev, "Failed to write shutdown_flag\n");
|
|
|
|
return NOTIFY_OK;
|
|
}
|
|
|
|
static void macsmc_power_init_error_counts(struct macsmc_reboot *reboot)
|
|
{
|
|
int boot_error_count, panic_count;
|
|
|
|
if (!reboot->nvm.boot_error_count || !reboot->nvm.panic_count)
|
|
return;
|
|
|
|
boot_error_count = nvmem_cell_get_u8(reboot->nvm.boot_error_count);
|
|
if (boot_error_count < 0) {
|
|
dev_err(reboot->dev, "Failed to read boot_error_count (%d)\n", boot_error_count);
|
|
return;
|
|
}
|
|
|
|
panic_count = nvmem_cell_get_u8(reboot->nvm.panic_count);
|
|
if (panic_count < 0) {
|
|
dev_err(reboot->dev, "Failed to read panic_count (%d)\n", panic_count);
|
|
return;
|
|
}
|
|
|
|
if (!boot_error_count && !panic_count)
|
|
return;
|
|
|
|
dev_warn(reboot->dev, "PMU logged %d boot error(s) and %d panic(s)\n",
|
|
boot_error_count, panic_count);
|
|
|
|
if (nvmem_cell_set_u8(reboot->nvm.panic_count, 0) < 0)
|
|
dev_err(reboot->dev, "Failed to reset panic_count\n");
|
|
if (nvmem_cell_set_u8(reboot->nvm.boot_error_count, 0) < 0)
|
|
dev_err(reboot->dev, "Failed to reset boot_error_count\n");
|
|
}
|
|
|
|
static int macsmc_reboot_probe(struct platform_device *pdev)
|
|
{
|
|
struct apple_smc *smc = dev_get_drvdata(pdev->dev.parent);
|
|
struct macsmc_reboot *reboot;
|
|
int ret, i;
|
|
|
|
reboot = devm_kzalloc(&pdev->dev, sizeof(*reboot), GFP_KERNEL);
|
|
if (!reboot)
|
|
return -ENOMEM;
|
|
|
|
reboot->dev = &pdev->dev;
|
|
reboot->smc = smc;
|
|
|
|
platform_set_drvdata(pdev, reboot);
|
|
|
|
for (i = 0; i < ARRAY_SIZE(nvmem_names); i++) {
|
|
struct nvmem_cell *cell;
|
|
|
|
cell = devm_nvmem_cell_get(&pdev->dev,
|
|
nvmem_names[i]);
|
|
if (IS_ERR(cell)) {
|
|
if (PTR_ERR(cell) == -EPROBE_DEFER)
|
|
return -EPROBE_DEFER;
|
|
dev_warn(&pdev->dev, "Missing NVMEM cell %s (%ld)\n",
|
|
nvmem_names[i], PTR_ERR(cell));
|
|
/* Non fatal, we'll deal with it */
|
|
cell = NULL;
|
|
}
|
|
reboot->nvm_cells[i] = cell;
|
|
}
|
|
|
|
/* Set the boot_stage to indicate we're running the OS kernel */
|
|
if (reboot->nvm.boot_stage &&
|
|
nvmem_cell_set_u8(reboot->nvm.boot_stage, BOOT_STAGE_KERNEL_STARTED) < 0)
|
|
dev_err(reboot->dev, "Failed to write boot_stage\n");
|
|
|
|
/* Display and clear the error counts */
|
|
macsmc_power_init_error_counts(reboot);
|
|
|
|
reboot->reboot_notify.notifier_call = macsmc_reboot_notify;
|
|
|
|
ret = devm_register_sys_off_handler(&pdev->dev, SYS_OFF_MODE_POWER_OFF_PREPARE,
|
|
SYS_OFF_PRIO_HIGH, macsmc_prepare_atomic, reboot);
|
|
if (ret)
|
|
return dev_err_probe(&pdev->dev, ret,
|
|
"Failed to register power-off prepare handler\n");
|
|
ret = devm_register_sys_off_handler(&pdev->dev, SYS_OFF_MODE_POWER_OFF, SYS_OFF_PRIO_HIGH,
|
|
macsmc_power_off, reboot);
|
|
if (ret)
|
|
return dev_err_probe(&pdev->dev, ret,
|
|
"Failed to register power-off handler\n");
|
|
|
|
ret = devm_register_sys_off_handler(&pdev->dev, SYS_OFF_MODE_RESTART_PREPARE,
|
|
SYS_OFF_PRIO_HIGH, macsmc_prepare_atomic, reboot);
|
|
if (ret)
|
|
return dev_err_probe(&pdev->dev, ret,
|
|
"Failed to register restart prepare handler\n");
|
|
ret = devm_register_sys_off_handler(&pdev->dev, SYS_OFF_MODE_RESTART, SYS_OFF_PRIO_HIGH,
|
|
macsmc_restart, reboot);
|
|
if (ret)
|
|
return dev_err_probe(&pdev->dev, ret, "Failed to register restart handler\n");
|
|
|
|
ret = devm_register_reboot_notifier(&pdev->dev, &reboot->reboot_notify);
|
|
if (ret)
|
|
return dev_err_probe(&pdev->dev, ret, "Failed to register reboot notifier\n");
|
|
|
|
dev_info(&pdev->dev, "Handling reboot and poweroff requests via SMC\n");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct of_device_id macsmc_reboot_of_table[] = {
|
|
{ .compatible = "apple,smc-reboot", },
|
|
{}
|
|
};
|
|
MODULE_DEVICE_TABLE(of, macsmc_reboot_of_table);
|
|
|
|
static struct platform_driver macsmc_reboot_driver = {
|
|
.driver = {
|
|
.name = "macsmc-reboot",
|
|
.of_match_table = macsmc_reboot_of_table,
|
|
},
|
|
.probe = macsmc_reboot_probe,
|
|
};
|
|
module_platform_driver(macsmc_reboot_driver);
|
|
|
|
MODULE_LICENSE("Dual MIT/GPL");
|
|
MODULE_DESCRIPTION("Apple SMC reboot/poweroff driver");
|
|
MODULE_AUTHOR("Hector Martin <marcan@marcan.st>");
|