cros_ec_python.devices.lpc

  1import struct
  2import warnings
  3import errno
  4import sys
  5
  6from ..baseclass import CrosEcClass
  7from ..constants.COMMON import *
  8from ..constants.LPC import *
  9from ..constants.MEMMAP import *
 10from ..exceptions import ECError
 11
 12from ..ioports import PortIO
 13
 14
 15class CrosEcLpc(CrosEcClass):
 16    """
 17    Class to interact with the EC using the LPC interface.
 18    """
 19
 20    def __init__(
 21        self, init: bool = True, address: Int32 = None, portio: PortIO | None = None
 22    ):
 23        """
 24        Detect and initialise the EC.
 25        :param init: Whether to initialise the EC on creation. Default is True.
 26        :param address: Specify a custom memmap address, will be detected if not specified.
 27        :param portio: PortIO object to use. Default is auto-detected.
 28        """
 29
 30        if portio is None:
 31            portio = PortIO()
 32
 33        self.portio: PortIO = portio
 34        """PortIO object to use."""
 35
 36        self.address: Int32 = address
 37        """The address of the EC memory map."""
 38
 39        if init:
 40            self.ec_init()
 41
 42    @staticmethod
 43    def detect() -> bool:
 44        """
 45        Checks for known CrOS EC memory map addresses in `/proc/ioports`.
 46        """
 47        if sys.platform == "linux":
 48            try:
 49                with open("/proc/ioports", "r") as f:
 50                    for line in f:
 51                        if line.lstrip()[:4] in (
 52                            format(EC_LPC_ADDR_MEMMAP, "04x"),
 53                            format(EC_LPC_ADDR_MEMMAP_FWAMD, "04x"),
 54                        ):
 55                            return True
 56                return False
 57            except FileNotFoundError:
 58                pass
 59        elif sys.platform == "win32":
 60            try:
 61                from wmi import WMI
 62
 63                c = WMI()
 64                for resource in c.Win32_PortResource():
 65                    start_address = int(resource.StartingAddress)
 66                    if start_address in (EC_LPC_ADDR_MEMMAP, EC_LPC_ADDR_MEMMAP_FWAMD):
 67                        return True
 68                return False
 69            except ImportError:
 70                pass
 71
 72        # Look for the memmap as a last resort
 73        return bool(
 74            CrosEcLpc.find_address(EC_LPC_ADDR_MEMMAP, EC_LPC_ADDR_MEMMAP_FWAMD)
 75        )
 76
 77    @staticmethod
 78    def find_address(*addresses, portio: PortIO | None = None) -> int | None:
 79        """
 80        Find the EC memory map address.
 81        :param addresses: A list of addresses to check.
 82        :return: The address of the EC memory map, or None if not found.
 83        """
 84        if portio is None:
 85            portio = PortIO()
 86        for a in addresses:
 87            if res := portio.ioperm(a, EC_MEMMAP_SIZE, True):
 88                if res == errno.EPERM:
 89                    raise PermissionError("Permission denied. Try running as root.")
 90                warnings.warn(
 91                    f"ioperm returned {errno.errorcode[res]} ({res}), skipping address {a}..."
 92                )
 93                continue
 94            # Check for 'EC' in memory map
 95            if portio.inw(a + EC_MEMMAP_ID) == int.from_bytes(b"EC", "little"):
 96                # Found it!
 97                return a
 98            else:
 99                # Nothing here
100                portio.ioperm(a, EC_MEMMAP_SIZE, False)
101                continue
102
103    def ec_init(self) -> None:
104        """
105        Initialise the EC. Checks for the EC, and configures the library to speak the same version.
106        :param address: Address of the EC memory map.
107        """
108        # Find memmap address
109        if self.address is None:
110            self.address = self.find_address(
111                EC_LPC_ADDR_MEMMAP, EC_LPC_ADDR_MEMMAP_FWAMD, portio=self.portio
112            )
113            # find_address will leave ioperm enabled for the memmap
114            if self.address is None:
115                raise OSError("Could not find EC!")
116
117        # Request I/O permissions
118        if (
119            (res := self.portio.ioperm(EC_LPC_ADDR_HOST_DATA, 1, True))
120            or (res := self.portio.ioperm(EC_LPC_ADDR_HOST_CMD, 1, True))
121            or (
122                res := self.portio.ioperm(
123                    EC_LPC_ADDR_HOST_PACKET, EC_LPC_HOST_PACKET_SIZE, True
124                )
125            )
126        ):
127            if res == errno.EPERM:
128                raise PermissionError("Permission denied. Try running as root.")
129            else:
130                raise OSError(f"ioperm returned {errno.errorcode[res]} ({res})")
131
132        status = 0xFF
133
134        # Read status bits, at least one should be 0
135        status &= self.portio.inb(EC_LPC_ADDR_HOST_CMD)
136        status &= self.portio.inb(EC_LPC_ADDR_HOST_DATA)
137
138        if status == 0xFF:
139            raise OSError("No EC detected. Invalid status.")
140
141        # Check for 'EC' in memory map
142        if self.portio.inw(self.address + EC_MEMMAP_ID) != int.from_bytes(
143            b"EC", "little"
144        ):
145            raise OSError("Invalid EC signature.")
146
147        self.ec_get_cmd_version()
148
149    def ec_exit(self) -> None:
150        pass
151
152    def wait_for_ec(self, status_addr: Int32 = EC_LPC_ADDR_HOST_CMD) -> None:
153        """
154        Wait for the EC to be ready after sending a command.
155        :param status_addr: The status register to read.
156        """
157        while self.portio.inb(status_addr) & EC_LPC_STATUS_BUSY_MASK:
158            pass
159
160    def ec_command_v2(
161        self,
162        version: UInt8,
163        command: UInt32,
164        outsize: UInt16,
165        insize: UInt32,
166        data: bytes = None,
167        warn: bool = True,
168    ):
169        """
170        Send a command to the EC and return the response. Uses the v2 command protocol over LPC. UNTESTED!
171        :param version: Command version number (often 0).
172        :param command: Command to send (EC_CMD_...).
173        :param outsize: Outgoing length in bytes.
174        :param insize: Max number of bytes to accept from the EC.
175        :param data: Outgoing data to EC.
176        :param warn: Whether to warn if the response size is not as expected. Default is True.
177        :return: Response from the EC.
178        """
179        warnings.warn(
180            "Support for v2 commands haven't been tested! Open an issue on github if it does "
181            "or doesn't work: https://github.com/Steve-Tech/CrOS_EC_Python/issues",
182            RuntimeWarning,
183        )
184        csum = 0
185        args = bytearray(
186            struct.pack("BBBB", EC_HOST_ARGS_FLAG_FROM_HOST, version, outsize, csum)
187        )
188        # (flags: UInt8, command_version: UInt8, data_size: UInt8, checksum: UInt8)
189
190        # Copy data and start checksum
191        for i in range(outsize):
192            self.portio.outb(data[i], EC_LPC_ADDR_HOST_PARAM + i)
193            csum += data[i]
194
195        # Finish checksum
196        for i in range(len(args)):
197            csum += args[i]
198
199        args[3] = csum & 0xFF
200
201        # Copy header
202        for i in range(len(args)):
203            self.portio.outb(args[i], EC_LPC_ADDR_HOST_ARGS + i)
204
205        # Start the command
206        self.portio.outb(command, EC_LPC_ADDR_HOST_CMD)
207
208        self.wait_for_ec()
209
210        # Check result
211        i = self.portio.inb(EC_LPC_ADDR_HOST_DATA)
212        if i:
213            raise ECError(i)
214
215        # Read back args
216        csum = 0
217        data_out = bytearray(len(args))
218        for i in range(len(data_out)):
219            data_out[i] = self.portio.inb(EC_LPC_ADDR_HOST_ARGS + i)
220            csum += data_out[i]
221
222        response = struct.unpack("BBBB", data_out)
223        # (flags: UInt8, command_version: UInt8, data_size: UInt8, checksum: UInt8)
224
225        if response[0] != EC_HOST_ARGS_FLAG_TO_HOST:
226            raise IOError("Invalid response!")
227
228        if response[2] != insize and warn:
229            warnings.warn(
230                f"Expected {insize} bytes, got {response[2]} back from EC",
231                RuntimeWarning,
232            )
233
234        # Read back data
235        data = bytearray()
236        for i in range(response[2]):
237            data.append(self.portio.inb(EC_LPC_ADDR_HOST_PARAM + i))
238            csum += data[i]
239
240        if response[3] != (csum & 0xFF):
241            raise IOError("Checksum error!")
242
243        return bytes(data)
244
245    def ec_command_v3(
246        self,
247        version: UInt8,
248        command: UInt32,
249        outsize: UInt16,
250        insize: UInt32,
251        data: bytes = None,
252        warn: bool = True,
253    ) -> bytes:
254        """
255        Send a command to the EC and return the response. Uses the v3 command protocol over LPC.
256        :param version: Command version number (often 0).
257        :param command: Command to send (EC_CMD_...).
258        :param outsize: Outgoing length in bytes.
259        :param insize: Max number of bytes to accept from the EC.
260        :param data: Outgoing data to EC.
261        :param warn: Whether to warn if the response size is not as expected. Default is True.
262        :return: Response from the EC.
263        """
264        csum = 0
265        request = bytearray(
266            struct.pack(
267                "BBHBxH", EC_HOST_REQUEST_VERSION, csum, command, version, outsize
268            )
269        )
270        # (struct_version: UInt8, checksum: UInt8, command: UInt16,
271        # command_version: UInt8, reserved: UInt8, data_len: UInt16)
272
273        # Fail if output size is too big
274        if outsize + len(request) > EC_LPC_HOST_PACKET_SIZE:
275            raise ValueError("Output size too big!")
276
277        # Copy data and start checksum
278        for i in range(outsize):
279            self.portio.outb(data[i], EC_LPC_ADDR_HOST_PACKET + len(request) + i)
280            csum += data[i]
281
282        # Finish checksum
283        for i in range(len(request)):
284            csum += request[i]
285
286        # Write checksum field so the entire packet sums to 0
287        request[1] = (-csum) & 0xFF
288
289        # Copy header
290        for i in range(len(request)):
291            self.portio.outb(request[i], EC_LPC_ADDR_HOST_PACKET + i)
292
293        # Start the command
294        self.portio.outb(EC_COMMAND_PROTOCOL_3, EC_LPC_ADDR_HOST_CMD)
295
296        self.wait_for_ec()
297
298        # Check result
299        i = self.portio.inb(EC_LPC_ADDR_HOST_DATA)
300        if i:
301            raise ECError(i)
302
303        # Read back response and start checksum
304        csum = 0
305        data_out = bytearray(struct.calcsize("BBHHH"))
306        for i in range(len(data_out)):
307            data_out[i] = self.portio.inb(EC_LPC_ADDR_HOST_PACKET + i)
308            csum += data_out[i]
309
310        response = struct.unpack("BBHHH", data_out)
311        # (struct_version: UInt8, checksum: UInt8, result: UInt16, data_len: UInt16, reserved: UInt16)
312
313        if response[0] != EC_HOST_RESPONSE_VERSION:
314            raise IOError("Invalid response version!")
315
316        if response[4]:
317            # Reserved should be 0
318            raise IOError("Invalid response!")
319
320        if response[3] != insize and warn:
321            warnings.warn(
322                f"Expected {insize} bytes, got {response[3]} back from EC",
323                RuntimeWarning,
324            )
325
326        # Read back data
327        data = bytearray()
328        for i in range(response[3]):
329            data.append(self.portio.inb(EC_LPC_ADDR_HOST_PACKET + len(data_out) + i))
330            csum += data[i]
331
332        if csum & 0xFF:
333            raise IOError("Checksum error!")
334
335        return bytes(data)
336
337    def command(self, *args):
338        """
339        Stub function, will get overwritten in ec_get_cmd_version.
340        """
341        raise NotImplementedError("EC doesn't support commands!")
342
343    def ec_get_cmd_version(self) -> int:
344        """
345        Find the version of the EC command protocol.
346        :return: The version of the EC command protocol.
347        """
348        version = self.portio.inb(self.address + EC_MEMMAP_HOST_CMD_FLAGS)
349
350        if version & EC_HOST_CMD_FLAG_VERSION_3:
351            self.command = self.ec_command_v3
352            return 3
353        elif version & EC_HOST_CMD_FLAG_LPC_ARGS_SUPPORTED:
354            self.command = self.ec_command_v2
355            return 2
356        else:
357            warnings.warn("EC doesn't support commands!", RuntimeWarning)
358            return 0
359
360    def memmap(self, offset: Int32, num_bytes: Int32) -> bytes:
361        """
362        Read memory from the EC.
363        :param offset: Offset to read from.
364        :param num_bytes: Number of bytes to read.
365        :param address: Address of the EC memory map.
366        :return: Bytes read from the EC.
367        """
368        data = bytearray()
369        for i in range(num_bytes):
370            data.append(self.portio.inb(self.address + offset + i))
371        return bytes(data)
class CrosEcLpc(cros_ec_python.baseclass.CrosEcClass):
 16class CrosEcLpc(CrosEcClass):
 17    """
 18    Class to interact with the EC using the LPC interface.
 19    """
 20
 21    def __init__(
 22        self, init: bool = True, address: Int32 = None, portio: PortIO | None = None
 23    ):
 24        """
 25        Detect and initialise the EC.
 26        :param init: Whether to initialise the EC on creation. Default is True.
 27        :param address: Specify a custom memmap address, will be detected if not specified.
 28        :param portio: PortIO object to use. Default is auto-detected.
 29        """
 30
 31        if portio is None:
 32            portio = PortIO()
 33
 34        self.portio: PortIO = portio
 35        """PortIO object to use."""
 36
 37        self.address: Int32 = address
 38        """The address of the EC memory map."""
 39
 40        if init:
 41            self.ec_init()
 42
 43    @staticmethod
 44    def detect() -> bool:
 45        """
 46        Checks for known CrOS EC memory map addresses in `/proc/ioports`.
 47        """
 48        if sys.platform == "linux":
 49            try:
 50                with open("/proc/ioports", "r") as f:
 51                    for line in f:
 52                        if line.lstrip()[:4] in (
 53                            format(EC_LPC_ADDR_MEMMAP, "04x"),
 54                            format(EC_LPC_ADDR_MEMMAP_FWAMD, "04x"),
 55                        ):
 56                            return True
 57                return False
 58            except FileNotFoundError:
 59                pass
 60        elif sys.platform == "win32":
 61            try:
 62                from wmi import WMI
 63
 64                c = WMI()
 65                for resource in c.Win32_PortResource():
 66                    start_address = int(resource.StartingAddress)
 67                    if start_address in (EC_LPC_ADDR_MEMMAP, EC_LPC_ADDR_MEMMAP_FWAMD):
 68                        return True
 69                return False
 70            except ImportError:
 71                pass
 72
 73        # Look for the memmap as a last resort
 74        return bool(
 75            CrosEcLpc.find_address(EC_LPC_ADDR_MEMMAP, EC_LPC_ADDR_MEMMAP_FWAMD)
 76        )
 77
 78    @staticmethod
 79    def find_address(*addresses, portio: PortIO | None = None) -> int | None:
 80        """
 81        Find the EC memory map address.
 82        :param addresses: A list of addresses to check.
 83        :return: The address of the EC memory map, or None if not found.
 84        """
 85        if portio is None:
 86            portio = PortIO()
 87        for a in addresses:
 88            if res := portio.ioperm(a, EC_MEMMAP_SIZE, True):
 89                if res == errno.EPERM:
 90                    raise PermissionError("Permission denied. Try running as root.")
 91                warnings.warn(
 92                    f"ioperm returned {errno.errorcode[res]} ({res}), skipping address {a}..."
 93                )
 94                continue
 95            # Check for 'EC' in memory map
 96            if portio.inw(a + EC_MEMMAP_ID) == int.from_bytes(b"EC", "little"):
 97                # Found it!
 98                return a
 99            else:
100                # Nothing here
101                portio.ioperm(a, EC_MEMMAP_SIZE, False)
102                continue
103
104    def ec_init(self) -> None:
105        """
106        Initialise the EC. Checks for the EC, and configures the library to speak the same version.
107        :param address: Address of the EC memory map.
108        """
109        # Find memmap address
110        if self.address is None:
111            self.address = self.find_address(
112                EC_LPC_ADDR_MEMMAP, EC_LPC_ADDR_MEMMAP_FWAMD, portio=self.portio
113            )
114            # find_address will leave ioperm enabled for the memmap
115            if self.address is None:
116                raise OSError("Could not find EC!")
117
118        # Request I/O permissions
119        if (
120            (res := self.portio.ioperm(EC_LPC_ADDR_HOST_DATA, 1, True))
121            or (res := self.portio.ioperm(EC_LPC_ADDR_HOST_CMD, 1, True))
122            or (
123                res := self.portio.ioperm(
124                    EC_LPC_ADDR_HOST_PACKET, EC_LPC_HOST_PACKET_SIZE, True
125                )
126            )
127        ):
128            if res == errno.EPERM:
129                raise PermissionError("Permission denied. Try running as root.")
130            else:
131                raise OSError(f"ioperm returned {errno.errorcode[res]} ({res})")
132
133        status = 0xFF
134
135        # Read status bits, at least one should be 0
136        status &= self.portio.inb(EC_LPC_ADDR_HOST_CMD)
137        status &= self.portio.inb(EC_LPC_ADDR_HOST_DATA)
138
139        if status == 0xFF:
140            raise OSError("No EC detected. Invalid status.")
141
142        # Check for 'EC' in memory map
143        if self.portio.inw(self.address + EC_MEMMAP_ID) != int.from_bytes(
144            b"EC", "little"
145        ):
146            raise OSError("Invalid EC signature.")
147
148        self.ec_get_cmd_version()
149
150    def ec_exit(self) -> None:
151        pass
152
153    def wait_for_ec(self, status_addr: Int32 = EC_LPC_ADDR_HOST_CMD) -> None:
154        """
155        Wait for the EC to be ready after sending a command.
156        :param status_addr: The status register to read.
157        """
158        while self.portio.inb(status_addr) & EC_LPC_STATUS_BUSY_MASK:
159            pass
160
161    def ec_command_v2(
162        self,
163        version: UInt8,
164        command: UInt32,
165        outsize: UInt16,
166        insize: UInt32,
167        data: bytes = None,
168        warn: bool = True,
169    ):
170        """
171        Send a command to the EC and return the response. Uses the v2 command protocol over LPC. UNTESTED!
172        :param version: Command version number (often 0).
173        :param command: Command to send (EC_CMD_...).
174        :param outsize: Outgoing length in bytes.
175        :param insize: Max number of bytes to accept from the EC.
176        :param data: Outgoing data to EC.
177        :param warn: Whether to warn if the response size is not as expected. Default is True.
178        :return: Response from the EC.
179        """
180        warnings.warn(
181            "Support for v2 commands haven't been tested! Open an issue on github if it does "
182            "or doesn't work: https://github.com/Steve-Tech/CrOS_EC_Python/issues",
183            RuntimeWarning,
184        )
185        csum = 0
186        args = bytearray(
187            struct.pack("BBBB", EC_HOST_ARGS_FLAG_FROM_HOST, version, outsize, csum)
188        )
189        # (flags: UInt8, command_version: UInt8, data_size: UInt8, checksum: UInt8)
190
191        # Copy data and start checksum
192        for i in range(outsize):
193            self.portio.outb(data[i], EC_LPC_ADDR_HOST_PARAM + i)
194            csum += data[i]
195
196        # Finish checksum
197        for i in range(len(args)):
198            csum += args[i]
199
200        args[3] = csum & 0xFF
201
202        # Copy header
203        for i in range(len(args)):
204            self.portio.outb(args[i], EC_LPC_ADDR_HOST_ARGS + i)
205
206        # Start the command
207        self.portio.outb(command, EC_LPC_ADDR_HOST_CMD)
208
209        self.wait_for_ec()
210
211        # Check result
212        i = self.portio.inb(EC_LPC_ADDR_HOST_DATA)
213        if i:
214            raise ECError(i)
215
216        # Read back args
217        csum = 0
218        data_out = bytearray(len(args))
219        for i in range(len(data_out)):
220            data_out[i] = self.portio.inb(EC_LPC_ADDR_HOST_ARGS + i)
221            csum += data_out[i]
222
223        response = struct.unpack("BBBB", data_out)
224        # (flags: UInt8, command_version: UInt8, data_size: UInt8, checksum: UInt8)
225
226        if response[0] != EC_HOST_ARGS_FLAG_TO_HOST:
227            raise IOError("Invalid response!")
228
229        if response[2] != insize and warn:
230            warnings.warn(
231                f"Expected {insize} bytes, got {response[2]} back from EC",
232                RuntimeWarning,
233            )
234
235        # Read back data
236        data = bytearray()
237        for i in range(response[2]):
238            data.append(self.portio.inb(EC_LPC_ADDR_HOST_PARAM + i))
239            csum += data[i]
240
241        if response[3] != (csum & 0xFF):
242            raise IOError("Checksum error!")
243
244        return bytes(data)
245
246    def ec_command_v3(
247        self,
248        version: UInt8,
249        command: UInt32,
250        outsize: UInt16,
251        insize: UInt32,
252        data: bytes = None,
253        warn: bool = True,
254    ) -> bytes:
255        """
256        Send a command to the EC and return the response. Uses the v3 command protocol over LPC.
257        :param version: Command version number (often 0).
258        :param command: Command to send (EC_CMD_...).
259        :param outsize: Outgoing length in bytes.
260        :param insize: Max number of bytes to accept from the EC.
261        :param data: Outgoing data to EC.
262        :param warn: Whether to warn if the response size is not as expected. Default is True.
263        :return: Response from the EC.
264        """
265        csum = 0
266        request = bytearray(
267            struct.pack(
268                "BBHBxH", EC_HOST_REQUEST_VERSION, csum, command, version, outsize
269            )
270        )
271        # (struct_version: UInt8, checksum: UInt8, command: UInt16,
272        # command_version: UInt8, reserved: UInt8, data_len: UInt16)
273
274        # Fail if output size is too big
275        if outsize + len(request) > EC_LPC_HOST_PACKET_SIZE:
276            raise ValueError("Output size too big!")
277
278        # Copy data and start checksum
279        for i in range(outsize):
280            self.portio.outb(data[i], EC_LPC_ADDR_HOST_PACKET + len(request) + i)
281            csum += data[i]
282
283        # Finish checksum
284        for i in range(len(request)):
285            csum += request[i]
286
287        # Write checksum field so the entire packet sums to 0
288        request[1] = (-csum) & 0xFF
289
290        # Copy header
291        for i in range(len(request)):
292            self.portio.outb(request[i], EC_LPC_ADDR_HOST_PACKET + i)
293
294        # Start the command
295        self.portio.outb(EC_COMMAND_PROTOCOL_3, EC_LPC_ADDR_HOST_CMD)
296
297        self.wait_for_ec()
298
299        # Check result
300        i = self.portio.inb(EC_LPC_ADDR_HOST_DATA)
301        if i:
302            raise ECError(i)
303
304        # Read back response and start checksum
305        csum = 0
306        data_out = bytearray(struct.calcsize("BBHHH"))
307        for i in range(len(data_out)):
308            data_out[i] = self.portio.inb(EC_LPC_ADDR_HOST_PACKET + i)
309            csum += data_out[i]
310
311        response = struct.unpack("BBHHH", data_out)
312        # (struct_version: UInt8, checksum: UInt8, result: UInt16, data_len: UInt16, reserved: UInt16)
313
314        if response[0] != EC_HOST_RESPONSE_VERSION:
315            raise IOError("Invalid response version!")
316
317        if response[4]:
318            # Reserved should be 0
319            raise IOError("Invalid response!")
320
321        if response[3] != insize and warn:
322            warnings.warn(
323                f"Expected {insize} bytes, got {response[3]} back from EC",
324                RuntimeWarning,
325            )
326
327        # Read back data
328        data = bytearray()
329        for i in range(response[3]):
330            data.append(self.portio.inb(EC_LPC_ADDR_HOST_PACKET + len(data_out) + i))
331            csum += data[i]
332
333        if csum & 0xFF:
334            raise IOError("Checksum error!")
335
336        return bytes(data)
337
338    def command(self, *args):
339        """
340        Stub function, will get overwritten in ec_get_cmd_version.
341        """
342        raise NotImplementedError("EC doesn't support commands!")
343
344    def ec_get_cmd_version(self) -> int:
345        """
346        Find the version of the EC command protocol.
347        :return: The version of the EC command protocol.
348        """
349        version = self.portio.inb(self.address + EC_MEMMAP_HOST_CMD_FLAGS)
350
351        if version & EC_HOST_CMD_FLAG_VERSION_3:
352            self.command = self.ec_command_v3
353            return 3
354        elif version & EC_HOST_CMD_FLAG_LPC_ARGS_SUPPORTED:
355            self.command = self.ec_command_v2
356            return 2
357        else:
358            warnings.warn("EC doesn't support commands!", RuntimeWarning)
359            return 0
360
361    def memmap(self, offset: Int32, num_bytes: Int32) -> bytes:
362        """
363        Read memory from the EC.
364        :param offset: Offset to read from.
365        :param num_bytes: Number of bytes to read.
366        :param address: Address of the EC memory map.
367        :return: Bytes read from the EC.
368        """
369        data = bytearray()
370        for i in range(num_bytes):
371            data.append(self.portio.inb(self.address + offset + i))
372        return bytes(data)

Class to interact with the EC using the LPC interface.

CrosEcLpc( init: bool = True, address: int = None, portio: cros_ec_python.ioports.x86portio.IoPortIo | None = None)
21    def __init__(
22        self, init: bool = True, address: Int32 = None, portio: PortIO | None = None
23    ):
24        """
25        Detect and initialise the EC.
26        :param init: Whether to initialise the EC on creation. Default is True.
27        :param address: Specify a custom memmap address, will be detected if not specified.
28        :param portio: PortIO object to use. Default is auto-detected.
29        """
30
31        if portio is None:
32            portio = PortIO()
33
34        self.portio: PortIO = portio
35        """PortIO object to use."""
36
37        self.address: Int32 = address
38        """The address of the EC memory map."""
39
40        if init:
41            self.ec_init()

Detect and initialise the EC.

Parameters
  • init: Whether to initialise the EC on creation. Default is True.
  • address: Specify a custom memmap address, will be detected if not specified.
  • portio: PortIO object to use. Default is auto-detected.

PortIO object to use.

address: int

The address of the EC memory map.

@staticmethod
def detect() -> bool:
43    @staticmethod
44    def detect() -> bool:
45        """
46        Checks for known CrOS EC memory map addresses in `/proc/ioports`.
47        """
48        if sys.platform == "linux":
49            try:
50                with open("/proc/ioports", "r") as f:
51                    for line in f:
52                        if line.lstrip()[:4] in (
53                            format(EC_LPC_ADDR_MEMMAP, "04x"),
54                            format(EC_LPC_ADDR_MEMMAP_FWAMD, "04x"),
55                        ):
56                            return True
57                return False
58            except FileNotFoundError:
59                pass
60        elif sys.platform == "win32":
61            try:
62                from wmi import WMI
63
64                c = WMI()
65                for resource in c.Win32_PortResource():
66                    start_address = int(resource.StartingAddress)
67                    if start_address in (EC_LPC_ADDR_MEMMAP, EC_LPC_ADDR_MEMMAP_FWAMD):
68                        return True
69                return False
70            except ImportError:
71                pass
72
73        # Look for the memmap as a last resort
74        return bool(
75            CrosEcLpc.find_address(EC_LPC_ADDR_MEMMAP, EC_LPC_ADDR_MEMMAP_FWAMD)
76        )

Checks for known CrOS EC memory map addresses in /proc/ioports.

@staticmethod
def find_address( *addresses, portio: cros_ec_python.ioports.x86portio.IoPortIo | None = None) -> int | None:
 78    @staticmethod
 79    def find_address(*addresses, portio: PortIO | None = None) -> int | None:
 80        """
 81        Find the EC memory map address.
 82        :param addresses: A list of addresses to check.
 83        :return: The address of the EC memory map, or None if not found.
 84        """
 85        if portio is None:
 86            portio = PortIO()
 87        for a in addresses:
 88            if res := portio.ioperm(a, EC_MEMMAP_SIZE, True):
 89                if res == errno.EPERM:
 90                    raise PermissionError("Permission denied. Try running as root.")
 91                warnings.warn(
 92                    f"ioperm returned {errno.errorcode[res]} ({res}), skipping address {a}..."
 93                )
 94                continue
 95            # Check for 'EC' in memory map
 96            if portio.inw(a + EC_MEMMAP_ID) == int.from_bytes(b"EC", "little"):
 97                # Found it!
 98                return a
 99            else:
100                # Nothing here
101                portio.ioperm(a, EC_MEMMAP_SIZE, False)
102                continue

Find the EC memory map address.

Parameters
  • addresses: A list of addresses to check.
Returns

The address of the EC memory map, or None if not found.

def ec_init(self) -> None:
104    def ec_init(self) -> None:
105        """
106        Initialise the EC. Checks for the EC, and configures the library to speak the same version.
107        :param address: Address of the EC memory map.
108        """
109        # Find memmap address
110        if self.address is None:
111            self.address = self.find_address(
112                EC_LPC_ADDR_MEMMAP, EC_LPC_ADDR_MEMMAP_FWAMD, portio=self.portio
113            )
114            # find_address will leave ioperm enabled for the memmap
115            if self.address is None:
116                raise OSError("Could not find EC!")
117
118        # Request I/O permissions
119        if (
120            (res := self.portio.ioperm(EC_LPC_ADDR_HOST_DATA, 1, True))
121            or (res := self.portio.ioperm(EC_LPC_ADDR_HOST_CMD, 1, True))
122            or (
123                res := self.portio.ioperm(
124                    EC_LPC_ADDR_HOST_PACKET, EC_LPC_HOST_PACKET_SIZE, True
125                )
126            )
127        ):
128            if res == errno.EPERM:
129                raise PermissionError("Permission denied. Try running as root.")
130            else:
131                raise OSError(f"ioperm returned {errno.errorcode[res]} ({res})")
132
133        status = 0xFF
134
135        # Read status bits, at least one should be 0
136        status &= self.portio.inb(EC_LPC_ADDR_HOST_CMD)
137        status &= self.portio.inb(EC_LPC_ADDR_HOST_DATA)
138
139        if status == 0xFF:
140            raise OSError("No EC detected. Invalid status.")
141
142        # Check for 'EC' in memory map
143        if self.portio.inw(self.address + EC_MEMMAP_ID) != int.from_bytes(
144            b"EC", "little"
145        ):
146            raise OSError("Invalid EC signature.")
147
148        self.ec_get_cmd_version()

Initialise the EC. Checks for the EC, and configures the library to speak the same version.

Parameters
  • address: Address of the EC memory map.
def ec_exit(self) -> None:
150    def ec_exit(self) -> None:
151        pass

Close connection to the EC.

def wait_for_ec(self, status_addr: int = 516) -> None:
153    def wait_for_ec(self, status_addr: Int32 = EC_LPC_ADDR_HOST_CMD) -> None:
154        """
155        Wait for the EC to be ready after sending a command.
156        :param status_addr: The status register to read.
157        """
158        while self.portio.inb(status_addr) & EC_LPC_STATUS_BUSY_MASK:
159            pass

Wait for the EC to be ready after sending a command.

Parameters
  • status_addr: The status register to read.
def ec_command_v2( self, version: int, command: int, outsize: int, insize: int, data: bytes = None, warn: bool = True):
161    def ec_command_v2(
162        self,
163        version: UInt8,
164        command: UInt32,
165        outsize: UInt16,
166        insize: UInt32,
167        data: bytes = None,
168        warn: bool = True,
169    ):
170        """
171        Send a command to the EC and return the response. Uses the v2 command protocol over LPC. UNTESTED!
172        :param version: Command version number (often 0).
173        :param command: Command to send (EC_CMD_...).
174        :param outsize: Outgoing length in bytes.
175        :param insize: Max number of bytes to accept from the EC.
176        :param data: Outgoing data to EC.
177        :param warn: Whether to warn if the response size is not as expected. Default is True.
178        :return: Response from the EC.
179        """
180        warnings.warn(
181            "Support for v2 commands haven't been tested! Open an issue on github if it does "
182            "or doesn't work: https://github.com/Steve-Tech/CrOS_EC_Python/issues",
183            RuntimeWarning,
184        )
185        csum = 0
186        args = bytearray(
187            struct.pack("BBBB", EC_HOST_ARGS_FLAG_FROM_HOST, version, outsize, csum)
188        )
189        # (flags: UInt8, command_version: UInt8, data_size: UInt8, checksum: UInt8)
190
191        # Copy data and start checksum
192        for i in range(outsize):
193            self.portio.outb(data[i], EC_LPC_ADDR_HOST_PARAM + i)
194            csum += data[i]
195
196        # Finish checksum
197        for i in range(len(args)):
198            csum += args[i]
199
200        args[3] = csum & 0xFF
201
202        # Copy header
203        for i in range(len(args)):
204            self.portio.outb(args[i], EC_LPC_ADDR_HOST_ARGS + i)
205
206        # Start the command
207        self.portio.outb(command, EC_LPC_ADDR_HOST_CMD)
208
209        self.wait_for_ec()
210
211        # Check result
212        i = self.portio.inb(EC_LPC_ADDR_HOST_DATA)
213        if i:
214            raise ECError(i)
215
216        # Read back args
217        csum = 0
218        data_out = bytearray(len(args))
219        for i in range(len(data_out)):
220            data_out[i] = self.portio.inb(EC_LPC_ADDR_HOST_ARGS + i)
221            csum += data_out[i]
222
223        response = struct.unpack("BBBB", data_out)
224        # (flags: UInt8, command_version: UInt8, data_size: UInt8, checksum: UInt8)
225
226        if response[0] != EC_HOST_ARGS_FLAG_TO_HOST:
227            raise IOError("Invalid response!")
228
229        if response[2] != insize and warn:
230            warnings.warn(
231                f"Expected {insize} bytes, got {response[2]} back from EC",
232                RuntimeWarning,
233            )
234
235        # Read back data
236        data = bytearray()
237        for i in range(response[2]):
238            data.append(self.portio.inb(EC_LPC_ADDR_HOST_PARAM + i))
239            csum += data[i]
240
241        if response[3] != (csum & 0xFF):
242            raise IOError("Checksum error!")
243
244        return bytes(data)

Send a command to the EC and return the response. Uses the v2 command protocol over LPC. UNTESTED!

Parameters
  • version: Command version number (often 0).
  • command: Command to send (EC_CMD_...).
  • outsize: Outgoing length in bytes.
  • insize: Max number of bytes to accept from the EC.
  • data: Outgoing data to EC.
  • warn: Whether to warn if the response size is not as expected. Default is True.
Returns

Response from the EC.

def ec_command_v3( self, version: int, command: int, outsize: int, insize: int, data: bytes = None, warn: bool = True) -> bytes:
246    def ec_command_v3(
247        self,
248        version: UInt8,
249        command: UInt32,
250        outsize: UInt16,
251        insize: UInt32,
252        data: bytes = None,
253        warn: bool = True,
254    ) -> bytes:
255        """
256        Send a command to the EC and return the response. Uses the v3 command protocol over LPC.
257        :param version: Command version number (often 0).
258        :param command: Command to send (EC_CMD_...).
259        :param outsize: Outgoing length in bytes.
260        :param insize: Max number of bytes to accept from the EC.
261        :param data: Outgoing data to EC.
262        :param warn: Whether to warn if the response size is not as expected. Default is True.
263        :return: Response from the EC.
264        """
265        csum = 0
266        request = bytearray(
267            struct.pack(
268                "BBHBxH", EC_HOST_REQUEST_VERSION, csum, command, version, outsize
269            )
270        )
271        # (struct_version: UInt8, checksum: UInt8, command: UInt16,
272        # command_version: UInt8, reserved: UInt8, data_len: UInt16)
273
274        # Fail if output size is too big
275        if outsize + len(request) > EC_LPC_HOST_PACKET_SIZE:
276            raise ValueError("Output size too big!")
277
278        # Copy data and start checksum
279        for i in range(outsize):
280            self.portio.outb(data[i], EC_LPC_ADDR_HOST_PACKET + len(request) + i)
281            csum += data[i]
282
283        # Finish checksum
284        for i in range(len(request)):
285            csum += request[i]
286
287        # Write checksum field so the entire packet sums to 0
288        request[1] = (-csum) & 0xFF
289
290        # Copy header
291        for i in range(len(request)):
292            self.portio.outb(request[i], EC_LPC_ADDR_HOST_PACKET + i)
293
294        # Start the command
295        self.portio.outb(EC_COMMAND_PROTOCOL_3, EC_LPC_ADDR_HOST_CMD)
296
297        self.wait_for_ec()
298
299        # Check result
300        i = self.portio.inb(EC_LPC_ADDR_HOST_DATA)
301        if i:
302            raise ECError(i)
303
304        # Read back response and start checksum
305        csum = 0
306        data_out = bytearray(struct.calcsize("BBHHH"))
307        for i in range(len(data_out)):
308            data_out[i] = self.portio.inb(EC_LPC_ADDR_HOST_PACKET + i)
309            csum += data_out[i]
310
311        response = struct.unpack("BBHHH", data_out)
312        # (struct_version: UInt8, checksum: UInt8, result: UInt16, data_len: UInt16, reserved: UInt16)
313
314        if response[0] != EC_HOST_RESPONSE_VERSION:
315            raise IOError("Invalid response version!")
316
317        if response[4]:
318            # Reserved should be 0
319            raise IOError("Invalid response!")
320
321        if response[3] != insize and warn:
322            warnings.warn(
323                f"Expected {insize} bytes, got {response[3]} back from EC",
324                RuntimeWarning,
325            )
326
327        # Read back data
328        data = bytearray()
329        for i in range(response[3]):
330            data.append(self.portio.inb(EC_LPC_ADDR_HOST_PACKET + len(data_out) + i))
331            csum += data[i]
332
333        if csum & 0xFF:
334            raise IOError("Checksum error!")
335
336        return bytes(data)

Send a command to the EC and return the response. Uses the v3 command protocol over LPC.

Parameters
  • version: Command version number (often 0).
  • command: Command to send (EC_CMD_...).
  • outsize: Outgoing length in bytes.
  • insize: Max number of bytes to accept from the EC.
  • data: Outgoing data to EC.
  • warn: Whether to warn if the response size is not as expected. Default is True.
Returns

Response from the EC.

def command(self, *args):
338    def command(self, *args):
339        """
340        Stub function, will get overwritten in ec_get_cmd_version.
341        """
342        raise NotImplementedError("EC doesn't support commands!")

Stub function, will get overwritten in ec_get_cmd_version.

def ec_get_cmd_version(self) -> int:
344    def ec_get_cmd_version(self) -> int:
345        """
346        Find the version of the EC command protocol.
347        :return: The version of the EC command protocol.
348        """
349        version = self.portio.inb(self.address + EC_MEMMAP_HOST_CMD_FLAGS)
350
351        if version & EC_HOST_CMD_FLAG_VERSION_3:
352            self.command = self.ec_command_v3
353            return 3
354        elif version & EC_HOST_CMD_FLAG_LPC_ARGS_SUPPORTED:
355            self.command = self.ec_command_v2
356            return 2
357        else:
358            warnings.warn("EC doesn't support commands!", RuntimeWarning)
359            return 0

Find the version of the EC command protocol.

Returns

The version of the EC command protocol.

def memmap(self, offset: int, num_bytes: int) -> bytes:
361    def memmap(self, offset: Int32, num_bytes: Int32) -> bytes:
362        """
363        Read memory from the EC.
364        :param offset: Offset to read from.
365        :param num_bytes: Number of bytes to read.
366        :param address: Address of the EC memory map.
367        :return: Bytes read from the EC.
368        """
369        data = bytearray()
370        for i in range(num_bytes):
371            data.append(self.portio.inb(self.address + offset + i))
372        return bytes(data)

Read memory from the EC.

Parameters
  • offset: Offset to read from.
  • num_bytes: Number of bytes to read.
  • address: Address of the EC memory map.
Returns

Bytes read from the EC.