Skip to content

asmc(4)

Source: sys/dev/asmc/asmc.c, sys/dev/asmc/asmcvar.h, sys/dev/asmc/asmcmmio.c, sys/dev/asmc/asmcmmio.h, sys/modules/asmc/.

The SMC is the microcontroller in every Intel Mac that owns fans, temperature sensors, the sudden motion sensor, the keyboard backlight, the sleep LED, and the power sequencing. The host talks to it through a keyed store: every value lives under a four-character key (TC0P, F0Ac, MSLD), and the driver speaks a small command protocol to read and write those keys.

Two backends, picked at probe time:

BackendTransportMachines
PIOI/O ports at base+0x00 (data), base+0x04 (command/status)Every pre-T2 Intel Mac
MMIOMemory window, command fired at 0x7F, status at 0x4005T2 Macs, where the PIO range is claimable but dead

The probe matches ACPI APP0001 and names the device from smbios.system.product, so devmatch autoloads it on anything Apple.

There is no per-model capability list. The old approach shipped a static table mapping every SMBIOS product string to its keys, which meant every new Mac was a driver patch. This driver asks the SMC instead:

The SMC’s own key directory is sorted, so asmc_detect_sensors binary searches to the start of each prefix region and walks forward with get-key-by-index. Every sysctl leaf exists only if its key answered a getinfo, so the tree shapes itself per machine. Static tables remain only for decoration: key descriptions (“TC0P” is “CPU Proximity”), fixed-point divisors per type (sp78 = 256, sp4b = 2048, …), and the shutdown-cause code names.

Status is the low nibble of the command port. Three values matter: 0x0c command accepted, 0x04 SMC awaits the next byte, 0x05 SMC has a byte ready.

The asymmetry: a read waits for 0x05 before each byte it takes, a write waits for 0x04 before each byte it gives. The length byte gets no wait of its own, the first data-phase wait covers it. Commands are 0x10 read, 0x11 write, 0x12 get-key-by-index, 0x13 getinfo (6-byte reply: length, 4-byte type, attributes).

The whole transaction runs under a spin mutex, polling with DELAY(). That is the price of sharing the lock with the SMS interrupt filter.

One shot per call, no outer retry loop. The result code comes back in the same register the command was written to.

T2 also changes the data: fan keys report type flt , 4-byte IEEE 754 little-endian, converted by kernel soft-float since there is no FPU context. Pre-T2 fans are fpe2 fixed point. Manual fan mode moves from the global FS! bitmask to a per-fan F%dMd key.

The one feature that is not a sensor. On a T2 Mac, bridgeOS watches the host. If the host goes down without saying goodbye, the watchdog calls it a crash and powers the machine back on. So a clean shutdown -p reboots instead of halting.

The handler registers at SHUTDOWN_PRI_FIRST on shutdown_pre_sync, so the SMC hears about the shutdown before the disks start syncing.

Init writes the threshold and interval keys, then hammers MOCN until it reads back 0xe0 0xf8, up to 1000 tries. Success arms the interrupt path:

The taskqueue runs at PI_REALTIME for the original reason SMS exists: park the disk heads before the laptop lands.

  • Keyboard backlight: light.control sysctl plus a backlight(9) registration, so backlight(8) works. The ambient light sensor key ALV0 comes in a 6-byte and a 10-byte format; attach detects which and picks the handler.
  • Sleep LED: sil sysctl. On is MSLS=1 then MSLD=1, off is MSLD=0xff alone.
  • Auto power-on: AUPO, boot when AC returns after power loss. Not wake-on-LAN.
  • Battery charge limit: BCLM, 0-100, T2 only.
  • System state: shutdown and sleep cause codes decoded through a table (5 is a good shutdown, -61 is a forced power-button hold), thermal status bitmap, board codename, security chip generation.
dev.asmc.0.
├── fan.N.{id, speed, safespeed, minspeed, maxspeed, targetspeed, manual}
├── temp.<KEY> one per detected sp78 key
├── voltage.<KEY> current.<KEY> power.<KEY> ambient.<KEY>
├── light.{left, right, control}
├── sms.{x, y, z}
├── auto_poweron sil battery_charge_limit
└── system.{shutdown_cause, sleep_cause, thermal_status,
time_of_day, power_state, board_id, chip_gen}

Every leaf is conditional on its key existing. Two Macs rarely show the same tree, and that is the point.

Terminal window
kldload asmc
sysctl dev.asmc.0.temp # every sp78 the SMC admits to
sysctl dev.asmc.0.fan.0.speed
sysctl dev.asmc.0.system.shutdown_cause
sysctl dev.asmc.0.sil=1 # sleep LED on, at full brightness