/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License (the "License").
* You may not use this file except in compliance with the License.
*
* You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
* or http://www.opensolaris.org/os/licensing.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at usr/src/OPENSOLARIS.LICENSE.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information: Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*/
/*
* Copyright 2009 Sun Microsystems, Inc. All rights reserved.
* Use is subject to license terms.
*/
/*
* Solaris x86 ACPI ThermalZone Monitor
*/
#include <sys/errno.h>
#include <sys/conf.h>
#include <sys/modctl.h>
#include <sys/open.h>
#include <sys/stat.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/ksynch.h>
#include <sys/uadmin.h>
#include <sys/acpi/acpi.h>
#include <sys/acpica.h>
#include <sys/sdt.h>
#include "tzmon.h"
#define TZMON_ENUM_TRIP_POINTS 1
#define TZMON_ENUM_DEV_LISTS 2
#define TZMON_ENUM_ALL (TZMON_ENUM_TRIP_POINTS | TZMON_ENUM_DEV_LISTS)
/*
* TZ_TASKQ_NAME_LEN is precisely the length of the string "AcpiThermalMonitor"
* plus a two-digit instance number plus a NULL. If the taskq name is changed
* (particularly if it is lengthened), then this value needs to change.
*/
#define TZ_TASKQ_NAME_LEN 21
/*
* Kelvin to Celsius conversion
* The formula for converting degrees Kelvin to degrees Celsius is
* C = K - 273.15 (we round to 273.2). The unit for thermal zone
* temperatures is tenths of a degree Kelvin. Use tenth of a degree
* to convert, then make a whole number out of it.
*/
#define K_TO_C(temp) (((temp) - 2732) / 10)
/* cb_ops or dev_ops forward declarations */
static int tzmon_getinfo(dev_info_t *dip, ddi_info_cmd_t infocmd,
void *arg, void **result);
static int tzmon_attach(dev_info_t *dip, ddi_attach_cmd_t cmd);
static int tzmon_detach(dev_info_t *dip, ddi_detach_cmd_t cmd);
/* other forward declarations */
static void tzmon_notify_zone(ACPI_HANDLE obj, UINT32 val, void *ctx);
static void tzmon_eval_int(ACPI_HANDLE obj, char *method, int *rv);
static thermal_zone_t *tzmon_alloc_zone();
static void tzmon_free_zone_list();
static void tzmon_discard_buffers(thermal_zone_t *tzp);
static void tzmon_enumerate_zone(ACPI_HANDLE obj, thermal_zone_t *tzp,
int enum_flag);
static ACPI_STATUS tzmon_zone_callback(ACPI_HANDLE obj, UINT32 nest,
void *ctx, void **rv);
static void tzmon_find_zones(void);
static void tzmon_monitor(void *ctx);
static void tzmon_set_power_device(ACPI_HANDLE dev, int on_off, char *tz_name);
static void tzmon_set_power(ACPI_BUFFER devlist, int on_off, char *tz_name);
static void tzmon_eval_zone(thermal_zone_t *tzp);
static void tzmon_do_shutdown(void);
extern void halt(char *);
static struct cb_ops tzmon_cb_ops = {
nodev, /* no open routine */
nodev, /* no close routine */
nodev, /* not a block driver */
nodev, /* no print routine */
nodev, /* no dump routine */
nodev, /* no read routine */
nodev, /* no write routine */
nodev, /* no ioctl routine */
nodev, /* no devmap routine */
nodev, /* no mmap routine */
nodev, /* no segmap routine */
nochpoll, /* no chpoll routine */
ddi_prop_op,
0, /* not a STREAMS driver */
D_NEW | D_MP, /* safe for multi-thread/multi-processor */
};
static struct dev_ops tzmon_ops = {
DEVO_REV, /* devo_rev */
0, /* devo_refcnt */
tzmon_getinfo, /* devo_getinfo */
nulldev, /* devo_identify */
nulldev, /* devo_probe */
tzmon_attach, /* devo_attach */
tzmon_detach, /* devo_detach */
nodev, /* devo_reset */
&tzmon_cb_ops, /* devo_cb_ops */
(struct bus_ops *)0, /* devo_bus_ops */
NULL, /* devo_power */
ddi_quiesce_not_needed, /* devo_quiesce */
};
extern struct mod_ops mod_driverops;
static struct modldrv modldrv = {
&mod_driverops,
"ACPI Thermal Zone Monitor",
&tzmon_ops,
};
static struct modlinkage modlinkage = {
MODREV_1, /* MODREV_1 indicated by manual */
(void *)&modldrv,
NULL, /* termination of list of linkage structures */
};
/* globals for this module */
static dev_info_t *tzmon_dip;
static thermal_zone_t *zone_list;
static int zone_count;
static kmutex_t zone_list_lock;
static kcondvar_t zone_list_condvar;
/*
* _init, _info, and _fini support loading and unloading the driver.
*/
int
_init(void)
{
return (mod_install(&modlinkage));
}
int
_info(struct modinfo *modinfop)
{
return (mod_info(&modlinkage, modinfop));
}
int
_fini(void)
{
return (mod_remove(&modlinkage));
}
static int
tzmon_attach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
if (cmd != DDI_ATTACH)
return (DDI_FAILURE);
if (tzmon_dip != NULL)
return (DDI_FAILURE);
/*
* Check to see if ACPI CA services are available
*/
if (AcpiSubsystemStatus() != AE_OK)
return (DDI_FAILURE);
mutex_init(&zone_list_lock, NULL, MUTEX_DRIVER, NULL);
cv_init(&zone_list_condvar, NULL, CV_DRIVER, NULL);
tzmon_find_zones();
mutex_enter(&zone_list_lock);
if (zone_count < 1) {
mutex_exit(&zone_list_lock);
mutex_destroy(&zone_list_lock);
cv_destroy(&zone_list_condvar);
return (DDI_FAILURE);
}
mutex_exit(&zone_list_lock);
if (ddi_create_minor_node(dip, ddi_get_name(dip), S_IFCHR, 0,
DDI_PSEUDO, 0) == DDI_FAILURE) {
tzmon_free_zone_list();
mutex_destroy(&zone_list_lock);
cv_destroy(&zone_list_condvar);
return (DDI_FAILURE);
}
tzmon_dip = dip;
ddi_report_dev(dip);
return (DDI_SUCCESS);
}
/*ARGSUSED*/
static int
tzmon_getinfo(dev_info_t *dip, ddi_info_cmd_t infocmd, void *arg, void **result)
{
int error;
switch (infocmd) {
case DDI_INFO_DEVT2DEVINFO:
*result = tzmon_dip;
if (tzmon_dip == NULL)
error = DDI_FAILURE;
else
error = DDI_SUCCESS;
break;
case DDI_INFO_DEVT2INSTANCE:
*result = 0;
error = DDI_SUCCESS;
break;
default:
*result = NULL;
error = DDI_FAILURE;
}
return (error);
}
static int
tzmon_detach(dev_info_t *dip, ddi_detach_cmd_t cmd)
{
thermal_zone_t *tzp = zone_list;
if (cmd != DDI_DETACH)
return (DDI_FAILURE);
/* free allocated thermal zone name(s) */
while (tzp != NULL) {
AcpiOsFree(tzp->zone_name);
tzp = tzp->next;
}
/* discard zone list assets */
tzmon_free_zone_list();
ddi_remove_minor_node(dip, NULL);
tzmon_dip = NULL;
mutex_destroy(&zone_list_lock);
cv_destroy(&zone_list_condvar);
return (DDI_SUCCESS);
}
/*
* tzmon_notify_zone
* Thermal zone notification handler.
*/
static void
tzmon_notify_zone(ACPI_HANDLE obj, UINT32 val, void *ctx)
{
thermal_zone_t *tzp = (thermal_zone_t *)ctx;
switch (val) {
case 0x80: /* Thermal Zone status changed */
tzmon_eval_zone(tzp);
break;
case 0x81: /* Thermal Zone trip points changed */
tzmon_enumerate_zone(obj, tzp, TZMON_ENUM_TRIP_POINTS);
break;
case 0x82: /* Device Lists changed */
tzmon_enumerate_zone(obj, tzp, TZMON_ENUM_DEV_LISTS);
break;
case 0x83: /* Thermal Relationship Table changed */
/* not handling _TRT objects, so not handling this event */
DTRACE_PROBE1(trt__change, char *, (char *)tzp->zone_name);
break;
default:
break;
}
}
/*
* tzmon_eval_int
* Evaluate the object/method as an integer.
*/
static void
tzmon_eval_int(ACPI_HANDLE obj, char *method, int *rv)
{
if (acpica_eval_int(obj, method, rv) != AE_OK)
*rv = -1;
}
/*
* tzmon_alloc_zone
* Allocate memory for the zone structure and initialize it lock mutex.
*/
static thermal_zone_t *
tzmon_alloc_zone()
{
thermal_zone_t *tzp;
tzp = kmem_zalloc(sizeof (thermal_zone_t), KM_SLEEP);
mutex_init(&tzp->lock, NULL, MUTEX_DRIVER, NULL);
return (tzp);
}
/*
* tzmon_free_zone_list
* Free the zone list, either because attach failed or detach initiated.
*/
static void
tzmon_free_zone_list()
{
thermal_zone_t *tzp = zone_list;
while (tzp != NULL) {
thermal_zone_t *next;
mutex_enter(&tzp->lock);
/*
* Remove the notify handler for the zone. Not much to
* do if this fails (since we are on our way out), so
* just ignore failure.
*/
(void) AcpiRemoveNotifyHandler(tzp->obj, ACPI_DEVICE_NOTIFY,
tzmon_notify_zone);
/* Shut down monitor thread, if running */
if (tzp->taskq != NULL) {
tzp->polling_period = 0;
cv_broadcast(&zone_list_condvar);
/* Drop mutex to allow the thread to run */
mutex_exit(&tzp->lock);
ddi_taskq_destroy(tzp->taskq);
mutex_enter(&tzp->lock);
}
tzmon_discard_buffers(tzp);
mutex_exit(&tzp->lock);
mutex_destroy(&tzp->lock);
next = tzp->next;
kmem_free(tzp, sizeof (thermal_zone_t));
tzp = next;
}
}
static void
tzmon_discard_buffers(thermal_zone_t *tzp)
{
int level;
for (level = 0; level < TZ_NUM_LEVELS; level++) {
if (tzp->al[level].Pointer != NULL)
AcpiOsFree(tzp->al[level].Pointer);
}
if (tzp->psl.Pointer != NULL)
AcpiOsFree(tzp->psl.Pointer);
}
/*
* tzmon_enumerate_zone
* Enumerates the contents of a thermal zone and updates passed-in
* thermal_zone or creates a new one if tzp is NULL. Newly-created
* zones are linked into the global zone_list.
*/
static void
tzmon_enumerate_zone(ACPI_HANDLE obj, thermal_zone_t *tzp, int enum_flag)
{
ACPI_STATUS status;
ACPI_BUFFER zone_name;
int level;
int instance;
char abuf[5];
/*
* Newly-created zones and existing zones both require
* some individual attention.
*/
if (tzp == NULL) {
/* New zone required */
tzp = tzmon_alloc_zone();
mutex_enter(&zone_list_lock);
tzp->next = zone_list;
zone_list = tzp;
/*
* It is exceedingly unlikely that instance will exceed 99.
* However, if it does, this will cause problems when
* creating the taskq for this thermal zone.
*/
instance = zone_count;
zone_count++;
mutex_exit(&zone_list_lock);
mutex_enter(&tzp->lock);
tzp->obj = obj;
/*
* Set to a low level. Will get set to the actual
* current power level when the thread monitor polls
* the current temperature.
*/
tzp->current_level = 0;
/* Get the zone name in case we need to display it later */
zone_name.Length = ACPI_ALLOCATE_BUFFER;
zone_name.Pointer = NULL;
status = AcpiGetName(obj, ACPI_FULL_PATHNAME, &zone_name);
ASSERT(status == AE_OK);
tzp->zone_name = zone_name.Pointer;
status = AcpiInstallNotifyHandler(obj, ACPI_DEVICE_NOTIFY,
tzmon_notify_zone, (void *)tzp);
ASSERT(status == AE_OK);
} else {
/* Existing zone - toss out allocated items */
mutex_enter(&tzp->lock);
ASSERT(tzp->obj == obj);
if (enum_flag & TZMON_ENUM_DEV_LISTS)
tzmon_discard_buffers(tzp);
}
if (enum_flag & TZMON_ENUM_TRIP_POINTS) {
for (level = 0; level < TZ_NUM_LEVELS; level++) {
(void) snprintf(abuf, 5, "_AC%d", level);
tzmon_eval_int(obj, abuf, &tzp->ac[level]);
}
tzmon_eval_int(obj, "_CRT", &tzp->crt);
tzmon_eval_int(obj, "_HOT", &tzp->hot);
tzmon_eval_int(obj, "_PSV", &tzp->psv);
}
if (enum_flag & TZMON_ENUM_DEV_LISTS) {
for (level = 0; level < TZ_NUM_LEVELS; level++) {
if (tzp->ac[level] == -1) {
tzp->al[level].Length = 0;
tzp->al[level].Pointer = NULL;
} else {
(void) snprintf(abuf, 5, "_AL%d", level);
tzp->al[level].Length = ACPI_ALLOCATE_BUFFER;
tzp->al[level].Pointer = NULL;
if (AcpiEvaluateObjectTyped(obj, abuf, NULL,
&tzp->al[level], ACPI_TYPE_PACKAGE) !=
AE_OK) {
DTRACE_PROBE2(alx__missing, int, level,
char *, (char *)tzp->zone_name);
tzp->al[level].Length = 0;
tzp->al[level].Pointer = NULL;
}
}
}
tzp->psl.Length = ACPI_ALLOCATE_BUFFER;
tzp->psl.Pointer = NULL;
(void) AcpiEvaluateObjectTyped(obj, "_PSL", NULL, &tzp->psl,
ACPI_TYPE_PACKAGE);
}
tzmon_eval_int(obj, "_TC1", &tzp->tc1);
tzmon_eval_int(obj, "_TC2", &tzp->tc2);
tzmon_eval_int(obj, "_TSP", &tzp->tsp);
tzmon_eval_int(obj, "_TZP", &tzp->tzp);
if (tzp->tzp == 0) {
tzp->polling_period = 0;
} else {
if (tzp->tzp < 0)
tzp->polling_period = TZ_DEFAULT_PERIOD;
else
tzp->polling_period = tzp->tzp/10;
/* start monitor thread if needed */
if (tzp->taskq == NULL) {
char taskq_name[TZ_TASKQ_NAME_LEN];
(void) snprintf(taskq_name, TZ_TASKQ_NAME_LEN,
"AcpiThermalMonitor%02d", instance);
tzp->taskq = ddi_taskq_create(tzmon_dip,
taskq_name, 1, TASKQ_DEFAULTPRI, 0);
if (tzp->taskq == NULL) {
tzp->polling_period = 0;
cmn_err(CE_WARN, "tzmon: could not create "
"monitor thread for thermal zone %s - "
"monitor by notify only",
(char *)tzp->zone_name);
} else {
(void) ddi_taskq_dispatch(tzp->taskq,
tzmon_monitor, tzp, DDI_SLEEP);
}
}
}
mutex_exit(&tzp->lock);
}
/*
* tzmon_zone_callback
* Enumerate the thermal zone if it has a _TMP (current thermal zone
* operating temperature) method.
*/
/*ARGSUSED*/
static ACPI_STATUS
tzmon_zone_callback(ACPI_HANDLE obj, UINT32 nest, void *ctx, void **rv)
{
ACPI_HANDLE tmpobj;
/*
* We get both ThermalZone() and Scope(\_TZ) objects here;
* look for _TMP (without which a zone is invalid) to pick
* between them (and ignore invalid zones)
*/
if (AcpiGetHandle(obj, "_TMP", &tmpobj) == AE_OK) {
tzmon_enumerate_zone(obj, NULL, TZMON_ENUM_ALL);
}
return (AE_OK);
}
/*
* tzmon_find_zones
* Find all of the thermal zones by calling a ACPICA function that
* walks the ACPI namespace and invokes a callback for each thermal
* object found.
*/
static void
tzmon_find_zones()
{
ACPI_STATUS status;
int retval;
status = AcpiWalkNamespace(ACPI_TYPE_THERMAL, ACPI_ROOT_OBJECT,
8, tzmon_zone_callback, NULL, NULL, (void **)&retval);
ASSERT(status == AE_OK);
}
/*
* tzmon_monitor
* Run as a separate thread, this wakes according to polling period and
* checks particular objects in the thermal zone. One instance per
* thermal zone.
*/
static void
tzmon_monitor(void *ctx)
{
thermal_zone_t *tzp = (thermal_zone_t *)ctx;
clock_t ticks;
do {
/* Check out the zone */
tzmon_eval_zone(tzp);
/* Go back to sleep */
mutex_enter(&tzp->lock);
ticks = drv_usectohz(tzp->polling_period * 1000000);
if (ticks > 0)
(void) cv_reltimedwait(&zone_list_condvar,
&tzp->lock, ticks, TR_CLOCK_TICK);
mutex_exit(&tzp->lock);
} while (ticks > 0);
}
/*
* tzmon_set_power_device
*/
static void
tzmon_set_power_device(ACPI_HANDLE dev, int on_off, char *tz_name)
{
ACPI_BUFFER rb;
ACPI_OBJECT *pr0;
ACPI_STATUS status;
int i;
rb.Length = ACPI_ALLOCATE_BUFFER;
rb.Pointer = NULL;
status = AcpiEvaluateObjectTyped(dev, "_PR0", NULL, &rb,
ACPI_TYPE_PACKAGE);
if (status != AE_OK) {
DTRACE_PROBE2(alx__error, int, 2, char *, tz_name);
return;
}
pr0 = ((ACPI_OBJECT *)rb.Pointer);
for (i = 0; i < pr0->Package.Count; i++) {
status = AcpiEvaluateObject(
pr0->Package.Elements[i].Reference.Handle,
on_off ? "_ON" : "_OFF", NULL, NULL);
if (status != AE_OK) {
DTRACE_PROBE2(alx__error, int, 4, char *, tz_name);
}
}
AcpiOsFree(rb.Pointer);
}
/*
* tzmon_set_power
* Turn on or turn off all devices in the supplied list.
*/
static void
tzmon_set_power(ACPI_BUFFER devlist, int on_off, char *tz_name)
{
ACPI_OBJECT *devs;
int i;
devs = ((ACPI_OBJECT *)devlist.Pointer);
if (devs->Type != ACPI_TYPE_PACKAGE) {
DTRACE_PROBE2(alx__error, int, 1, char *, tz_name);
return;
}
for (i = 0; i < devs->Package.Count; i++)
tzmon_set_power_device(
devs->Package.Elements[i].Reference.Handle, on_off,
tz_name);
}
/*
* tzmon_eval_zone
* Evaluate the current conditions within the thermal zone.
*/
static void
tzmon_eval_zone(thermal_zone_t *tzp)
{
int tmp, new_level, level;
mutex_enter(&tzp->lock);
/* get the current temperature from ACPI */
tzmon_eval_int(tzp->obj, "_TMP", &tmp);
DTRACE_PROBE4(tz__temp, int, tmp, int, tzp->crt, int, tzp->hot,
char *, (char *)tzp->zone_name);
/* _HOT handling */
if (tzp->hot > 0 && tmp >= tzp->hot) {
cmn_err(CE_WARN,
"tzmon: Thermal zone (%s) is too hot (%d C); "
"initiating shutdown\n",
(char *)tzp->zone_name, K_TO_C(tmp));
tzmon_do_shutdown();
}
/* _CRT handling */
if (tzp->crt > 0 && tmp >= tzp->crt) {
cmn_err(CE_WARN,
"tzmon: Thermal zone (%s) is critically hot (%d C); "
"initiating rapid shutdown\n",
(char *)tzp->zone_name, K_TO_C(tmp));
/* shut down (fairly) immediately */
mdboot(A_REBOOT, AD_HALT, NULL, B_FALSE);
}
/*
* use the temperature to determine whether the thermal zone
* is at a new active cooling threshold level
*/
for (level = 0, new_level = -1; level < TZ_NUM_LEVELS; level++) {
if (tzp->ac[level] >= 0 && (tmp >= tzp->ac[level])) {
new_level = level;
break;
}
}
/*
* if the active cooling threshold has changed, turn off the
* devices associated with the old one and turn on the new one
*/
if (tzp->current_level != new_level) {
if ((tzp->current_level >= 0) &&
(tzp->al[tzp->current_level].Length != 0))
tzmon_set_power(tzp->al[tzp->current_level], 0,
(char *)tzp->zone_name);
if ((new_level >= 0) &&
(tzp->al[new_level].Length != 0))
tzmon_set_power(tzp->al[new_level], 1,
(char *)tzp->zone_name);
tzp->current_level = new_level;
}
mutex_exit(&tzp->lock);
}
/*
* tzmon_do_shutdown
* Initiates shutdown by sending a SIGPWR signal to init.
*/
static void
tzmon_do_shutdown(void)
{
proc_t *initpp;
mutex_enter(&pidlock);
initpp = prfind(P_INITPID);
mutex_exit(&pidlock);
/* if we can't find init, just halt */
if (initpp == NULL) {
mdboot(A_REBOOT, AD_HALT, NULL, B_FALSE);
}
/* graceful shutdown with inittab and all getting involved */
psignal(initpp, SIGPWR);
}