404 lines
12 KiB
Python
404 lines
12 KiB
Python
"""liquidctl drivers for DDR4 memory.
|
||
|
||
Copyright (C) 2020–2021 Jonas Malaco and contributors
|
||
SPDX-License-Identifier: GPL-3.0-or-later
|
||
"""
|
||
|
||
import itertools
|
||
import logging
|
||
from collections import namedtuple
|
||
from enum import Enum, unique
|
||
|
||
from liquidctl.driver.smbus import SmbusDriver
|
||
from liquidctl.error import ExpectationNotMet, NotSupportedByDevice, NotSupportedByDriver
|
||
from liquidctl.util import RelaxedNamesEnum, check_unsafe, clamp
|
||
|
||
_LOGGER = logging.getLogger(__name__)
|
||
|
||
|
||
class Ddr4Spd:
|
||
"""Partial decoding of DDR4 Serial Presence Detect (SPD) information.
|
||
|
||
Properties will raise on data they are not yet prepared to handle, but what
|
||
is implemented attempts to comply with JEDEC 21-C 4.1.2 Annex L.
|
||
"""
|
||
|
||
class DramDeviceType(Enum):
|
||
"""DRAM device type (not exhaustive)."""
|
||
DDR4_SDRAM = 0x0c
|
||
LPDDR4_SDRAM = 0x10
|
||
LPDDR4X_SDRAM = 0x11
|
||
|
||
def __str__(self):
|
||
return self.name.replace('_', ' ')
|
||
|
||
class BaseModuleType(Enum):
|
||
"""Base module type (not exhaustive)."""
|
||
|
||
RDIMM = 0b0001
|
||
UDIMM = 0b0010
|
||
SO_DIMM = 0b0011
|
||
LRDIMM = 0x0100
|
||
|
||
def __str__(self):
|
||
return self.name.replace('_', ' ')
|
||
|
||
# Standard Manufacturer's Identification Code from JEDEC JEP106;
|
||
# (not exhaustive) maps banks and IDs to names: _JEP106[<bank>][id]
|
||
_JEP106 = {
|
||
1: {
|
||
0x2c: 'Micron',
|
||
0xad: 'SK Hynix',
|
||
0xce: 'Samsung',
|
||
},
|
||
2: {0x98: 'Kingston'},
|
||
3: {0x9e: 'Corsair'},
|
||
5: {
|
||
0xcd: 'G.SKILL',
|
||
0xef: 'Team Group',
|
||
},
|
||
6: {
|
||
0x02: 'Patriot',
|
||
0x9b: 'Crucial',
|
||
},
|
||
}
|
||
|
||
def __init__(self, eeprom):
|
||
self._eeprom = eeprom
|
||
|
||
if self.dram_device_type not in [self.DramDeviceType.DDR4_SDRAM,
|
||
self.DramDeviceType.LPDDR4_SDRAM,
|
||
self.DramDeviceType.LPDDR4X_SDRAM]:
|
||
raise ValueError('not a DDR4 SPD EEPROM')
|
||
|
||
@property
|
||
def spd_bytes_used(self):
|
||
nibble = self._eeprom[0x00] & 0x0f
|
||
assert nibble <= 0b0100, 'reserved'
|
||
return nibble * 128
|
||
|
||
@property
|
||
def spd_bytes_total(self):
|
||
nibble = (self._eeprom[0x00] >> 4) & 0b111
|
||
assert nibble <= 0b010, 'reserved'
|
||
return nibble * 256
|
||
|
||
@property
|
||
def spd_revision(self):
|
||
enc_level = self._eeprom[0x01] >> 4
|
||
add_level = self._eeprom[0x01] & 0x0f
|
||
return (enc_level, add_level)
|
||
|
||
@property
|
||
def dram_device_type(self):
|
||
return self.DramDeviceType(self._eeprom[0x02])
|
||
|
||
@property
|
||
def module_type(self):
|
||
base = self._eeprom[0x03] & 0x0f
|
||
hybrid = self._eeprom[0x03] >> 4
|
||
assert not hybrid
|
||
return (self.BaseModuleType(base), None)
|
||
|
||
@property
|
||
def module_thermal_sensor(self):
|
||
present = self._eeprom[0x0e] >> 7
|
||
return bool(present)
|
||
|
||
@property
|
||
def module_manufacturer(self):
|
||
bank = 1 + self._eeprom[0x140] & 0x7f
|
||
mid = self._eeprom[0x141]
|
||
return self._JEP106[bank][mid]
|
||
|
||
@property
|
||
def module_part_number(self):
|
||
return self._eeprom[0x149:0x15d].decode(encoding='ascii').rstrip()
|
||
|
||
@property
|
||
def dram_manufacturer(self):
|
||
bank = 1 + self._eeprom[0x15e] & 0x7f
|
||
mid = self._eeprom[0x15f]
|
||
return self._JEP106[bank][mid]
|
||
|
||
|
||
class Ddr4Temperature(SmbusDriver):
|
||
"""DDR4 module with TSE2004-compatible SPD EEPROM and temperature sensor."""
|
||
|
||
_SPD_DTIC = 0x50
|
||
_TS_DTIC = 0x18
|
||
_SA_MASK = 0b111
|
||
_REG_CAPABILITIES = 0x00
|
||
_REG_TEMPERATURE = 0x05
|
||
|
||
_UNSAFE = ['smbus', 'ddr4_temperature']
|
||
|
||
@classmethod
|
||
def probe(cls, smbus, vendor=None, product=None, address=None, match=None,
|
||
release=None, serial=None, **kwargs):
|
||
|
||
# FIXME support mainstream AMD chipsets on Linux; note that unlike
|
||
# i801_smbus, piix4_smbus does not enumerate and register the available
|
||
# SPD EEPROMs with i2c_register_spd
|
||
|
||
_SMBUS_DRIVERS = ['i801_smbus']
|
||
|
||
if smbus.parent_driver not in _SMBUS_DRIVERS \
|
||
or any([vendor, product, release, serial]): # wont match, always None
|
||
return
|
||
|
||
for dimm in range(cls._SA_MASK + 1):
|
||
spd_addr = cls._SPD_DTIC | dimm
|
||
eeprom = smbus.load_eeprom(spd_addr)
|
||
|
||
if not eeprom or eeprom.name != 'ee1004':
|
||
continue
|
||
|
||
try:
|
||
spd = Ddr4Spd(eeprom.data)
|
||
|
||
if spd.dram_device_type != Ddr4Spd.DramDeviceType.DDR4_SDRAM:
|
||
continue
|
||
|
||
desc = cls._match(spd)
|
||
except:
|
||
continue
|
||
|
||
if not desc:
|
||
continue
|
||
|
||
desc += f' DIMM{dimm + 1} (experimental)'
|
||
|
||
if (address and int(address, base=16) != spd_addr) \
|
||
or (match and match.lower() not in desc.lower()):
|
||
continue
|
||
|
||
# set the default device address to a weird value to prevent
|
||
# accidental attempts of writes to the SPD EEPROM (DDR4 SPD writes
|
||
# are also disabled by default in many motherboards)
|
||
dev = cls(smbus, desc, address=(None, None, spd_addr))
|
||
_LOGGER.debug('instanced driver for %s', desc)
|
||
yield dev
|
||
|
||
@classmethod
|
||
def _match(cls, spd):
|
||
if not spd.module_thermal_sensor:
|
||
return None
|
||
|
||
try:
|
||
manufacturer = spd.module_manufacturer
|
||
except:
|
||
return 'DDR4'
|
||
|
||
if spd.module_part_number:
|
||
return f'{manufacturer} {spd.module_part_number}'
|
||
else:
|
||
return manufacturer
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
self._ts_address = self._TS_DTIC | (self._address[2] & self._SA_MASK)
|
||
|
||
@property
|
||
def address(self):
|
||
return f'{self._address[2]:#04x}'
|
||
|
||
def get_status(self, **kwargs):
|
||
"""Get a status report.
|
||
|
||
Returns a list of `(property, value, unit)` tuples.
|
||
"""
|
||
|
||
if not check_unsafe(*self._UNSAFE, **kwargs):
|
||
_LOGGER.warning("%s: nothing returned, requires unsafe features '%s'",
|
||
self.description, ','.join(self._UNSAFE))
|
||
return []
|
||
|
||
treg = self._read_temperature_register()
|
||
|
||
# discard flags bits and interpret remaining bits as 2s complement
|
||
treg = treg & 0x1fff
|
||
if treg > 0x0fff:
|
||
treg -= 0x2000
|
||
|
||
# should always be supported
|
||
resolution, bits = (.25, 10)
|
||
|
||
multiplier = treg >> (12 - bits)
|
||
|
||
return [
|
||
('Temperature', resolution * multiplier, '°C'),
|
||
]
|
||
|
||
def _read_temperature_register(self):
|
||
return self._smbus.read_block_data(self._ts_address, self._REG_TEMPERATURE)
|
||
|
||
def initialize(self, **kwargs):
|
||
"""Initialize the device."""
|
||
pass
|
||
|
||
def set_color(self, channel, mode, colors, **kwargs):
|
||
"""Not supported by this driver."""
|
||
raise NotSupportedByDriver()
|
||
|
||
def set_speed_profile(self, channel, profile, **kwargs):
|
||
"""Not supported by this device."""
|
||
raise NotSupportedByDevice()
|
||
|
||
def set_fixed_speed(self, channel, duty, **kwargs):
|
||
"""Not supported by this device."""
|
||
raise NotSupportedByDevice()
|
||
|
||
|
||
class VengeanceRgb(Ddr4Temperature):
|
||
"""Corsair Vengeance RGB DDR4 module."""
|
||
|
||
_RGB_DTIC = 0x58
|
||
_REG_RGB_TIMING1 = 0xa4
|
||
_REG_RGB_TIMING2 = 0xa5
|
||
_REG_RGB_MODE = 0xa6
|
||
_REG_RGB_COLOR_COUNT = 0xa7
|
||
_REG_RGB_COLOR_START = 0xb0
|
||
_REG_RGB_COLOR_END = 0xc5
|
||
|
||
_UNSAFE = ['smbus', 'vengeance_rgb']
|
||
|
||
@unique
|
||
class Mode(bytes, RelaxedNamesEnum):
|
||
def __new__(cls, value, min_colors, max_colors):
|
||
obj = bytes.__new__(cls, [value])
|
||
obj._value_ = value
|
||
obj.min_colors = min_colors
|
||
obj.max_colors = max_colors
|
||
return obj
|
||
|
||
FIXED = (0x00, 1, 1)
|
||
FADING = (0x01, 2, 7)
|
||
BREATHING = (0x02, 1, 7)
|
||
|
||
OFF = (0xf0, 0, 0) # pseudo mode, equivalent to fixed #000000
|
||
|
||
def __str__(self):
|
||
return self.name.lower()
|
||
|
||
@unique
|
||
class SpeedTimings(RelaxedNamesEnum):
|
||
SLOWEST = 63
|
||
SLOWER = 48
|
||
NORMAL = 32
|
||
FASTER = 16
|
||
FASTEST = 1
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
self._rgb_address = None
|
||
|
||
@classmethod
|
||
def _match(cls, spd):
|
||
if spd.module_type != (Ddr4Spd.BaseModuleType.UDIMM, None) \
|
||
or spd.module_manufacturer != 'Corsair' \
|
||
or not spd.module_part_number.startswith('CMR'):
|
||
return None
|
||
|
||
return 'Corsair Vengeance RGB'
|
||
|
||
def _read_temperature_register(self):
|
||
# instead of using block reads, Vengeance RGB temperature sensor
|
||
# devices must be read in words
|
||
treg = self._smbus.read_word_data(self._ts_address, self._REG_TEMPERATURE)
|
||
|
||
# swap LSB and MSB before returning: read_word_data reads in little
|
||
# endianess, but the register should be read in big endianess
|
||
return ((treg & 0xff) << 8) | (treg >> 8)
|
||
|
||
def set_color(self, channel, mode, colors, speed='normal',
|
||
transition_ticks=None, stable_ticks=None, **kwargs):
|
||
"""Set the RGB lighting mode and, when applicable, color.
|
||
|
||
The table bellow summarizes the available channels, modes and their
|
||
associated number of required colors.
|
||
|
||
| Channel | Mode | Colors |
|
||
| -------- | --------- | ------ |
|
||
| led | off | 0 |
|
||
| led | fixed | 1 |
|
||
| led | breathing | 1–7 |
|
||
| led | fading | 2–7 |
|
||
|
||
The speed of the breathing and fading animations can be adjusted with
|
||
`speed`; the allowed values are 'slowest', 'slower', 'normal'
|
||
(default), 'faster' and 'fastest'.
|
||
|
||
It is also possible to override the raw timing parameters through
|
||
`transition_ticks` and `stable_ticks`; these should be integer values
|
||
in the range 0–63.
|
||
"""
|
||
|
||
check_unsafe(*self._UNSAFE, error=True, **kwargs)
|
||
|
||
try:
|
||
common = self.SpeedTimings[speed].value
|
||
tp1 = tp2 = common
|
||
except KeyError:
|
||
raise ValueError(f'invalid speed preset: {speed!r}') from None
|
||
|
||
if transition_ticks is not None:
|
||
tp1 = clamp(transition_ticks, 0, 63)
|
||
if stable_ticks is not None:
|
||
tp2 = clamp(stable_ticks, 0, 63)
|
||
|
||
colors = list(colors)
|
||
|
||
try:
|
||
mode = self.Mode[mode]
|
||
except KeyError:
|
||
raise ValueError(f'invalid mode: {mode!r}') from None
|
||
|
||
if len(colors) < mode.min_colors:
|
||
raise ValueError(f'{mode} mode requires {mode.min_colors} colors')
|
||
|
||
if len(colors) > mode.max_colors:
|
||
_LOGGER.debug('too many colors, dropping to %d', mode.max_colors)
|
||
colors = colors[:mode.max_colors]
|
||
|
||
self._compute_rgb_address()
|
||
|
||
if mode == self.Mode.OFF:
|
||
mode = self.Mode.FIXED
|
||
colors = [[0x00, 0x00, 0x00]]
|
||
|
||
def rgb_write(register, value):
|
||
self._smbus.write_byte_data(self._rgb_address, register, value)
|
||
|
||
if mode == self.Mode.FIXED:
|
||
rgb_write(self._REG_RGB_TIMING1, 0x00)
|
||
else:
|
||
rgb_write(self._REG_RGB_TIMING1, tp1)
|
||
rgb_write(self._REG_RGB_TIMING2, tp2)
|
||
|
||
color_registers = range(self._REG_RGB_COLOR_START, self._REG_RGB_COLOR_END)
|
||
color_components = itertools.chain(*colors)
|
||
|
||
for register, component in zip(color_registers, color_components):
|
||
rgb_write(register, component)
|
||
|
||
rgb_write(self._REG_RGB_COLOR_COUNT, len(colors))
|
||
|
||
if mode == self.Mode.BREATHING and len(colors) == 1:
|
||
rgb_write(self._REG_RGB_MODE, self.Mode.FIXED.value)
|
||
else:
|
||
rgb_write(self._REG_RGB_MODE, mode.value)
|
||
|
||
def _compute_rgb_address(self):
|
||
if self._rgb_address:
|
||
return
|
||
|
||
# the dimm's rgb controller is typically at 0x58–0x5f
|
||
candidate = self._RGB_DTIC | (self._address[2] & self._SA_MASK)
|
||
|
||
# reading from any register should return 0xba if we have the right device
|
||
if self._smbus.read_byte_data(candidate, self._REG_RGB_MODE) != 0xba:
|
||
raise ExpectationNotMet(f'{self.bus}:{candidate:#04x} is not the RGB controller')
|
||
|
||
self._rgb_address = candidate
|