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)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.