/* * 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 #include #include #include #include #include #include #include #include #include #include #include #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, (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); }