asmc: add automatic voltage/current/power/ambient sensor detection

Apple SMCs contain numerous undocumented voltage, current, power,
and ambient light sensors. This change adds automatic detection
and registration of these sensors as sysctls.

New sysctl trees:
  dev.asmc.0.voltage.*  - Voltage sensors (millivolts)
  dev.asmc.0.current.*  - Current sensors (milliamps)
  dev.asmc.0.power.*    - Power sensors (milliwatts)
  dev.asmc.0.ambient.*  - Ambient light sensors

Implementation:
- Scans all SMC keys at attach time via asmc_key_dump_by_index()
- Identifies sensors by key prefix patterns:
  - Voltage: VC*, VD*, VG*, VP*, VI*
  - Current: I{C,D,G,M,N,O,H,P,B,A,L}*
  - Power:   P{C,D,N,S,T,H,F,Z,z}*
  - Light:   ALV*, ALS*
- Dynamically creates sysctls for detected sensors
- Supports 8 fixed-point SMC data types:
  - sp78, sp87, sp4b, sp5a, sp69, sp96, sp2d, ui16
- Auto-converts all values to milli-units (mV, mA, mW)

On Mac Mini 5,1, detects:
- 7 voltage sensors
- 18 current sensors
- 27 power sensors
- 2 ambient light sensors

Enables power consumption monitoring, voltage rail debugging,
and ambient light detection without hardcoding model-specific
sensor lists.

Tested on:
  - Mac Mini 5,1 (2011) running FreeBSD 15.0-RELEASE
  - 54 sensors auto-detected and exposed via sysctl
  - All sensor types verified with multimeter readings
  - Fixed-point conversions validated against known values
  - Memory management tested (malloc/free on detach)

Reviewed by:	adrian
Differential Revision:	https://reviews.freebsd.org/D55807
This commit is contained in:
Abdelkader Boudih
2026-04-16 19:31:21 -07:00
committed by Adrian Chadd
parent b49b3ccd40
commit aae9068404
2 changed files with 445 additions and 2 deletions
+435 -2
View File
@@ -123,15 +123,22 @@ static int asmc_mbp_sysctl_light_control(SYSCTL_HANDLER_ARGS);
static int asmc_mbp_sysctl_light_left_10byte(SYSCTL_HANDLER_ARGS);
static int asmc_wol_sysctl(SYSCTL_HANDLER_ARGS);
static int asmc_key_getinfo(device_t, const char *, uint8_t *, char *);
#ifdef ASMC_DEBUG
/* Raw key access */
static int asmc_key_getinfo(device_t, const char *, uint8_t *, char *);
static int asmc_raw_key_sysctl(SYSCTL_HANDLER_ARGS);
static int asmc_raw_value_sysctl(SYSCTL_HANDLER_ARGS);
static int asmc_raw_len_sysctl(SYSCTL_HANDLER_ARGS);
static int asmc_raw_type_sysctl(SYSCTL_HANDLER_ARGS);
#endif
/* Voltage/Current/Power/Light sensor support */
static int asmc_sensor_read(device_t, const char *, int *);
static int asmc_sensor_sysctl(SYSCTL_HANDLER_ARGS);
static int asmc_detect_sensors(device_t);
static int asmc_key_dump_by_index(device_t, int, char *, char *, uint8_t *);
struct asmc_model {
const char *smc_model; /* smbios.system.product env var. */
const char *smc_desc; /* driver description */
@@ -963,6 +970,16 @@ asmc_detach(device_t dev)
if (sc->sc_kbd_bkl != NULL)
backlight_destroy(sc->sc_kbd_bkl);
/* Free sensor key arrays */
for (int i = 0; i < sc->sc_voltage_count; i++)
free(sc->sc_voltage_sensors[i], M_DEVBUF);
for (int i = 0; i < sc->sc_current_count; i++)
free(sc->sc_current_sensors[i], M_DEVBUF);
for (int i = 0; i < sc->sc_power_count; i++)
free(sc->sc_power_sensors[i], M_DEVBUF);
for (int i = 0; i < sc->sc_light_count; i++)
free(sc->sc_light_sensors[i], M_DEVBUF);
if (sc->sc_sms_tq) {
taskqueue_drain(sc->sc_sms_tq, &sc->sc_sms_task);
taskqueue_free(sc->sc_sms_tq);
@@ -1134,6 +1151,12 @@ asmc_init(device_t dev)
sc->sc_nkeys = 0;
}
/*
* Auto-detect and register voltage/current/power/ambient sensors.
* Scans SMC keys and creates sysctls for detected sensors.
*/
asmc_detect_sensors(dev);
out_err:
#ifdef ASMC_DEBUG
asmc_dumpall(dev);
@@ -1337,10 +1360,10 @@ asmc_key_dump(device_t dev, int number)
return (0);
}
#endif /* ASMC_DEBUG */
/*
* Get key info (length and type) from SMC using command 0x13.
* Returns 0 on success, -1 on failure.
* If len is non-NULL, stores the key's value length.
* If type is non-NULL, stores the 4-char type string (must be at least 5 bytes).
*/
@@ -1389,6 +1412,7 @@ asmc_key_getinfo(device_t dev, const char *key, uint8_t *len, char *type)
return (error);
}
#ifdef ASMC_DEBUG
/*
* Raw SMC key access sysctls - enables reading/writing any SMC key by name
* Usage:
@@ -1491,6 +1515,415 @@ asmc_raw_type_sysctl(SYSCTL_HANDLER_ARGS)
}
#endif
/*
* Convert signed fixed-point SMC values to milli-units.
* Format "spXY" means signed with X integer bits and Y fraction bits.
*/
static int
asmc_sp78_to_milli(const uint8_t *buf)
{
int16_t val = (int16_t)be16dec(buf);
return ((int)val * 1000) / 256;
}
static int
asmc_sp87_to_milli(const uint8_t *buf)
{
int16_t val = (int16_t)be16dec(buf);
return ((int)val * 1000) / 128;
}
static int
asmc_sp4b_to_milli(const uint8_t *buf)
{
int16_t val = (int16_t)be16dec(buf);
return ((int)val * 1000) / 2048;
}
static int
asmc_sp5a_to_milli(const uint8_t *buf)
{
int16_t val = (int16_t)be16dec(buf);
return ((int)val * 1000) / 1024;
}
static int
asmc_sp69_to_milli(const uint8_t *buf)
{
int16_t val = (int16_t)be16dec(buf);
return ((int)val * 1000) / 512;
}
static int
asmc_sp96_to_milli(const uint8_t *buf)
{
int16_t val = (int16_t)be16dec(buf);
return ((int)val * 1000) / 64;
}
static int
asmc_sp2d_to_milli(const uint8_t *buf)
{
int16_t val = (int16_t)be16dec(buf);
return ((int)val * 1000) / 8192;
}
static bool
asmc_sensor_type_supported(const char *type)
{
return (strncmp(type, "sp78", 4) == 0 ||
strncmp(type, "sp87", 4) == 0 ||
strncmp(type, "sp4b", 4) == 0 ||
strncmp(type, "sp5a", 4) == 0 ||
strncmp(type, "sp69", 4) == 0 ||
strncmp(type, "sp96", 4) == 0 ||
strncmp(type, "sp2d", 4) == 0 ||
strncmp(type, "ui16", 4) == 0);
}
/*
* Generic sensor value reader with automatic type conversion.
* Reads an SMC key, detects its type, and converts to millivalue.
*/
static int
asmc_sensor_read(device_t dev, const char *key, int *millivalue)
{
uint8_t buf[2];
char type[ASMC_TYPELEN + 1];
uint8_t len;
int error;
error = asmc_key_getinfo(dev, key, &len, type);
if (error != 0)
return (error);
if (len != 2) {
if (bootverbose)
device_printf(dev,
"%s: key %s unexpected length %d\n",
__func__, key, len);
return (ENXIO);
}
error = asmc_key_read(dev, key, buf, sizeof(buf));
if (error != 0)
return (error);
if (strncmp(type, "sp78", 4) == 0) {
*millivalue = asmc_sp78_to_milli(buf);
} else if (strncmp(type, "sp87", 4) == 0) {
*millivalue = asmc_sp87_to_milli(buf);
} else if (strncmp(type, "sp4b", 4) == 0) {
*millivalue = asmc_sp4b_to_milli(buf);
} else if (strncmp(type, "sp5a", 4) == 0) {
*millivalue = asmc_sp5a_to_milli(buf);
} else if (strncmp(type, "sp69", 4) == 0) {
*millivalue = asmc_sp69_to_milli(buf);
} else if (strncmp(type, "sp96", 4) == 0) {
*millivalue = asmc_sp96_to_milli(buf);
} else if (strncmp(type, "sp2d", 4) == 0) {
*millivalue = asmc_sp2d_to_milli(buf);
} else if (strncmp(type, "ui16", 4) == 0) {
*millivalue = be16dec(buf);
} else {
if (bootverbose)
device_printf(dev,
"%s: unknown type '%s' for key %s\n",
__func__, type, key);
return (ENXIO);
}
return (0);
}
/*
* Generic sensor sysctl handler for voltage/current/power/light sensors.
* arg2 encodes: sensor_type (high byte) | sensor_index (low byte)
* Sensor types: 'V'=voltage, 'I'=current, 'P'=power, 'L'=light
*/
static int
asmc_sensor_sysctl(SYSCTL_HANDLER_ARGS)
{
device_t dev = (device_t) arg1;
struct asmc_softc *sc = device_get_softc(dev);
int error, val;
int sensor_type = (arg2 >> 8) & 0xFF;
int sensor_idx = arg2 & 0xFF;
const char *key = NULL;
/* Select sensor based on type and index */
switch (sensor_type) {
case 'V': /* Voltage */
if (sensor_idx < sc->sc_voltage_count)
key = sc->sc_voltage_sensors[sensor_idx];
break;
case 'I': /* Current */
if (sensor_idx < sc->sc_current_count)
key = sc->sc_current_sensors[sensor_idx];
break;
case 'P': /* Power */
if (sensor_idx < sc->sc_power_count)
key = sc->sc_power_sensors[sensor_idx];
break;
case 'L': /* Light */
if (sensor_idx < sc->sc_light_count)
key = sc->sc_light_sensors[sensor_idx];
break;
default:
return (EINVAL);
}
if (key == NULL)
return (ENOENT);
error = asmc_sensor_read(dev, key, &val);
if (error != 0)
return (error);
return (sysctl_handle_int(oidp, &val, 0, req));
}
/*
* Detect and register voltage/current/power/ambient sensors.
* Scans all SMC keys and identifies sensor keys by prefix.
* Returns 0 on success, -1 on error.
*/
static int
asmc_detect_sensors(device_t dev)
{
struct asmc_softc *sc = device_get_softc(dev);
struct sysctl_ctx_list *sysctlctx;
struct sysctl_oid *tree_node;
char key[ASMC_KEYLEN + 1];
char type[ASMC_TYPELEN + 1];
uint8_t len;
unsigned int nkeys;
unsigned int i;
int error;
char *sensor_key;
/* Initialize counts */
sc->sc_voltage_count = 0;
sc->sc_current_count = 0;
sc->sc_power_count = 0;
sc->sc_light_count = 0;
if (sc->sc_nkeys == 0)
return (0);
nkeys = sc->sc_nkeys;
/* Scan all keys for voltage/current/power/ambient light sensors */
for (i = 0; i < nkeys; i++) {
/* Get key name by index */
error = asmc_key_dump_by_index(dev, i, key, type, &len);
if (error != 0)
continue;
if (!asmc_sensor_type_supported(type))
continue;
/* Voltage sensors (VC*, VD*, VG*, VP*, VI*) */
if (key[0] == 'V' && (key[1] == 'C' || key[1] == 'D' ||
key[1] == 'G' || key[1] == 'P' || key[1] == 'I') &&
len == 2) {
if (sc->sc_voltage_count >= ASMC_MAX_SENSORS)
continue;
sensor_key = malloc(ASMC_KEYLEN + 1,
M_DEVBUF, M_WAITOK);
memcpy(sensor_key, key, ASMC_KEYLEN + 1);
sc->sc_voltage_sensors[sc->sc_voltage_count++] =
sensor_key;
} else if (key[0] == 'I' && (key[1] == 'C' ||
key[1] == 'D' || key[1] == 'G' || key[1] == 'M' ||
key[1] == 'N' || key[1] == 'O' || key[1] == 'H' ||
key[1] == 'P' || key[1] == 'B' || key[1] == 'A' ||
key[1] == 'L') && len == 2) {
/* Current sensors */
if (sc->sc_current_count >= ASMC_MAX_SENSORS)
continue;
sensor_key = malloc(ASMC_KEYLEN + 1,
M_DEVBUF, M_WAITOK);
memcpy(sensor_key, key, ASMC_KEYLEN + 1);
sc->sc_current_sensors[sc->sc_current_count++] =
sensor_key;
} else if (key[0] == 'P' && (key[1] == 'C' ||
key[1] == 'D' || key[1] == 'N' || key[1] == 'S' ||
key[1] == 'T' || key[1] == 'H' || key[1] == 'F' ||
key[1] == 'Z' || key[1] == 'z') && len == 2) {
/* Power sensors */
if (sc->sc_power_count >= ASMC_MAX_SENSORS)
continue;
sensor_key = malloc(ASMC_KEYLEN + 1,
M_DEVBUF, M_WAITOK);
memcpy(sensor_key, key, ASMC_KEYLEN + 1);
sc->sc_power_sensors[sc->sc_power_count++] =
sensor_key;
} else if (key[0] == 'A' && key[1] == 'L' &&
(key[2] == 'V' || key[2] == 'S') && len == 2) {
/* Ambient light sensors */
if (sc->sc_light_count >= ASMC_MAX_SENSORS)
continue;
sensor_key = malloc(ASMC_KEYLEN + 1,
M_DEVBUF, M_WAITOK);
memcpy(sensor_key, key, ASMC_KEYLEN + 1);
sc->sc_light_sensors[sc->sc_light_count++] =
sensor_key;
}
}
if (bootverbose)
device_printf(dev,
"detected %d voltage, %d current, "
"%d power, %d light sensors\n",
sc->sc_voltage_count, sc->sc_current_count,
sc->sc_power_count, sc->sc_light_count);
/* Register sysctls for detected sensors */
sysctlctx = device_get_sysctl_ctx(dev);
/* Voltage sensors */
if (sc->sc_voltage_count > 0) {
tree_node = SYSCTL_ADD_NODE(sysctlctx,
SYSCTL_CHILDREN(device_get_sysctl_tree(dev)), OID_AUTO,
"voltage", CTLFLAG_RD | CTLFLAG_MPSAFE, 0, "Voltage sensors (millivolts)");
for (i = 0; i < sc->sc_voltage_count; i++) {
SYSCTL_ADD_PROC(sysctlctx, SYSCTL_CHILDREN(tree_node),
OID_AUTO, sc->sc_voltage_sensors[i],
CTLTYPE_INT | CTLFLAG_RD | CTLFLAG_MPSAFE,
dev, ('V' << 8) | i, asmc_sensor_sysctl, "I",
"Voltage sensor (millivolts)");
}
}
/* Current sensors */
if (sc->sc_current_count > 0) {
tree_node = SYSCTL_ADD_NODE(sysctlctx,
SYSCTL_CHILDREN(device_get_sysctl_tree(dev)), OID_AUTO,
"current", CTLFLAG_RD | CTLFLAG_MPSAFE, 0, "Current sensors (milliamps)");
for (i = 0; i < sc->sc_current_count; i++) {
SYSCTL_ADD_PROC(sysctlctx, SYSCTL_CHILDREN(tree_node),
OID_AUTO, sc->sc_current_sensors[i],
CTLTYPE_INT | CTLFLAG_RD | CTLFLAG_MPSAFE,
dev, ('I' << 8) | i, asmc_sensor_sysctl, "I",
"Current sensor (milliamps)");
}
}
/* Power sensors */
if (sc->sc_power_count > 0) {
tree_node = SYSCTL_ADD_NODE(sysctlctx,
SYSCTL_CHILDREN(device_get_sysctl_tree(dev)), OID_AUTO,
"power", CTLFLAG_RD | CTLFLAG_MPSAFE, 0, "Power sensors (milliwatts)");
for (i = 0; i < sc->sc_power_count; i++) {
SYSCTL_ADD_PROC(sysctlctx, SYSCTL_CHILDREN(tree_node),
OID_AUTO, sc->sc_power_sensors[i],
CTLTYPE_INT | CTLFLAG_RD | CTLFLAG_MPSAFE,
dev, ('P' << 8) | i, asmc_sensor_sysctl, "I",
"Power sensor (milliwatts)");
}
}
/* Ambient light sensors */
if (sc->sc_light_count > 0) {
tree_node = SYSCTL_ADD_NODE(sysctlctx,
SYSCTL_CHILDREN(device_get_sysctl_tree(dev)), OID_AUTO,
"ambient", CTLFLAG_RD | CTLFLAG_MPSAFE, 0, "Ambient light sensors");
for (i = 0; i < sc->sc_light_count; i++) {
SYSCTL_ADD_PROC(sysctlctx, SYSCTL_CHILDREN(tree_node),
OID_AUTO, sc->sc_light_sensors[i],
CTLTYPE_INT | CTLFLAG_RD | CTLFLAG_MPSAFE,
dev, ('L' << 8) | i, asmc_sensor_sysctl, "I",
"Light sensor value");
}
}
return (0);
}
/*
* Helper function to get key info by index (for sensor detection).
*/
static int
asmc_key_dump_by_index(device_t dev, int index, char *key_out,
char *type_out, uint8_t *len_out)
{
struct asmc_softc *sc = device_get_softc(dev);
uint8_t index_buf[ASMC_KEYLEN];
uint8_t key_buf[ASMC_KEYLEN];
uint8_t info_buf[ASMC_KEYINFO_RESPLEN];
int error = ENXIO, try = 0;
int i;
mtx_lock_spin(&sc->sc_mtx);
index_buf[0] = (index >> 24) & 0xff;
index_buf[1] = (index >> 16) & 0xff;
index_buf[2] = (index >> 8) & 0xff;
index_buf[3] = index & 0xff;
begin:
if (asmc_command(dev, ASMC_CMDGETBYINDEX))
goto out;
for (i = 0; i < ASMC_KEYLEN; i++) {
ASMC_DATAPORT_WRITE(sc, index_buf[i]);
if (asmc_wait(dev, ASMC_STATUS_AWAIT_DATA))
goto out;
}
ASMC_DATAPORT_WRITE(sc, ASMC_KEYLEN);
for (i = 0; i < ASMC_KEYLEN; i++) {
if (asmc_wait(dev, ASMC_STATUS_DATA_READY))
goto out;
key_buf[i] = ASMC_DATAPORT_READ(sc);
}
if (asmc_command(dev, ASMC_CMDGETINFO))
goto out;
for (i = 0; i < ASMC_KEYLEN; i++) {
ASMC_DATAPORT_WRITE(sc, key_buf[i]);
if (asmc_wait(dev, ASMC_STATUS_AWAIT_DATA))
goto out;
}
ASMC_DATAPORT_WRITE(sc, ASMC_KEYINFO_RESPLEN);
for (i = 0; i < ASMC_KEYINFO_RESPLEN; i++) {
if (asmc_wait(dev, ASMC_STATUS_DATA_READY))
goto out;
info_buf[i] = ASMC_DATAPORT_READ(sc);
}
memcpy(key_out, key_buf, ASMC_KEYLEN);
key_out[ASMC_KEYLEN] = '\0';
*len_out = info_buf[0];
memcpy(type_out, &info_buf[1], ASMC_TYPELEN);
type_out[ASMC_TYPELEN] = '\0';
error = 0;
out:
if (error) {
if (++try < ASMC_MAXRETRIES)
goto begin;
}
mtx_unlock_spin(&sc->sc_mtx);
return (error);
}
static int
asmc_key_write(device_t dev, const char *key, uint8_t *buf, uint8_t len)
{
+10
View File
@@ -31,6 +31,7 @@
#define ASMC_MAXVAL 32 /* Maximum SMC value size */
#define ASMC_KEYLEN 4 /* SMC key name length */
#define ASMC_TYPELEN 4 /* SMC type string length */
#define ASMC_MAX_SENSORS 64 /* Max sensors per type */
struct asmc_softc {
device_t sc_dev;
@@ -64,6 +65,15 @@ struct asmc_softc {
uint8_t sc_rawlen;
char sc_rawtype[ASMC_TYPELEN + 1];
#endif
/* Voltage/Current/Power/Light sensors */
char *sc_voltage_sensors[ASMC_MAX_SENSORS];
int sc_voltage_count;
char *sc_current_sensors[ASMC_MAX_SENSORS];
int sc_current_count;
char *sc_power_sensors[ASMC_MAX_SENSORS];
int sc_power_count;
char *sc_light_sensors[ASMC_MAX_SENSORS];
int sc_light_count;
};
/*