The Framework Laptop is probably the coolest laptop of the century. It’s also got a pretty cool embedded controller, which this post details.

NOTE: This information was gathered in part from Framework UEFI 3.07. No warranty is expressed or implied. Use at your own risk.

The original version of this document was written in December 2021 and based on reverse engineering the embedded controller firmware. That version is available here.

The EC in the Framework Laptop is based on the CrOS EC originally developed by Google for their ChromeOS devices.

It is available as an open-source project at FrameworkComputer/EmbeddedController (branch hx20).

User-toggleable features such as the battery charge limit and the power button brightness are copied from NVRAM to the EC by OEMBIOSSyncToECDxe on every boot. Changes made by interacting directly with the EC will not be retained across boots.

Protocol

The embedded controller uses the protocol documented in the Microchip MEC172x data sheet to exchange v3 commands as described in the EC-3PO docs.

To send a command:

  1. Write 0x0000 | 0x03 (offset 0, autoincrementing 32-bit I/O) to 0x802 (16-bit).
  2. Write data, 32 bits at a time, to 0x804 and 0x806.
    • For data longer than 32 bits, repeat writes to 0x804 and 0x806.
    • For data shorter than 32 bits, write it byte-by-byte into 0x804, 0x805, 0x806 and 0x807.
  3. Write 0xDA to 0x204.

Using fw-ectool

  1. If you’re using a kernel that supports lockdown and secure boot is enabled, make sure you turn it off. This application uses raw port I/O and requires a higher I/O privilege level, which is locked down when secure boot is enabled
  2. Clone the repository.
  3. make utils. This will produce an ectool binary at $/build/bds/util/ectool.
  4. .../ectool version
  5. Go wild!

Raw Communication

fw-ectool offers a new command, raw, which allows you to send undocumented or unsupported commands to the EC. It will handle encoding the packet as a v3 exchange.

Syntax: raw 0xCOMMAND payload payload takes the form of a long string of type codes (b, w, d, q) and values in hex.

Examples:

  • b1 will write one byte, 0x01.
  • w3 will write one word in little-endian, 0x03 0x00.
  • b1w3 or b1,w3 will write 0x01 0x03 0x00.

For a more realistic example of its use, see the Keyboard Mapping section.

Sample

$ ectool version
RO version:    hx20_v0.0.1-369d3c3
RW version:
Firmware copy: RO
Build info:    hx20_v0.0.1-369d3c3 2021-12-13 21:47:58 runner@fv-az209-518
Tool version:  v2.0.11601-7e9447fef 2021-12-22 11:25:36 dustin@rigel

Using CrosEC (Windows)

WARNING! The content in this section is not complete, and enabling test signing can break games and other applications that rely on DRM.

  1. Download a release from the CrosEC releases page
  2. Disable Secure Boot and enable Test Signing
    • bcdedit /set {default} testsigning on
    • You cannot toggle test signing while Secure Boot is enabled!
  3. Install CrosEC.sys using devcon
    • devcon install CrosEC.inf ROOT\CrosEC

CrOS EC Commands

This section documents upstream commands that are known to work on Framework’s embedded controller.

led

The led command supports the power, left and right LEDs.

ectool ... led left blue
ectool ... led power green

The power LED does not support blue.

Fan Duty

The fanduty (set fan duty percentage) and autofanctrl (restore fan control to automatic) commands both appear to work and control the main system fan.

Thanks to D.H for the report.

OEM Commands

This section documents OEM commands that Framework Computer has added to their build of ChromeOS’s embedded controller firmware.

NOTE: Assume all structures are packed as with __attribute__((packed)) or __pragma(pack(1))

3E01 - “Flash Notified”

Source

#define EC_CMD_FLASH_NOTIFIED 0x3E01

enum ec_flash_notified_flags {
	FLASH_ACCESS_SPI	  = 0,
	FLASH_FIRMWARE_START  = BIT(0),
	FLASH_FIRMWARE_DONE   = BIT(1),
	FLASH_ACCESS_SPI_DONE = 3,
	FLASH_FLAG_PD         = BIT(4),
};

This host command enables/disables flashing. Kieran noted on Discord that flash is locked because it shares pins with one of the device’s PWM channels.

The power button is disabled for the duration that flash is unlocked.

During startup, OEMBIOSSyncToECDxe calls this function to unlock flash and write the first-boot date to the factory info flash region.

3E02 - Factory Mode

Source

#define EC_CMD_FACTORY_MODE	0x3E02
#define RESET_FOR_SHIP 0x5A

struct ec_params_factory_notified {
	uint8_t flags; // 0x0, 0x1, 0x5A
} __ec_align1;

This host command toggles a keyboard testing mode that is used to validate devices in the factory and clear the chassis intrusion stage afterwards.

Invoking this command with the flag 0x01 remaps the Fn key to emit scancode E016 and the fingerprint power key to emit scancode E025. The onboard power switch is not impacted.

Calling it again with flag value 0x00 restores the original functions of both the Fn key and the power key.

Calling it with flag RESET_FOR_SHIP (0x5A, ASCII Z) both disables the Fn key as in 0x01 and zaps the battery charge limit and chassis intrusion count in the EC’s non-volatile memory.

3E03 - Charge Limit Control

Source

#define EC_CMD_CHARGE_LIMIT_CONTROL 0x3E03
#define NEED_RESTORE 0x7F

enum ec_chg_limit_control_modes {
	CHG_LIMIT_DISABLE	= BIT(0),
	CHG_LIMIT_SET_LIMIT	= BIT(1),
	CHG_LIMIT_GET_LIMIT	= BIT(3),
	CHG_LIMIT_OVERRIDE	= BIT(7),
};

struct ec_params_ec_chg_limit_control {
	uint8_t modes;
	uint8_t max_percentage;
	uint8_t min_percentage;
} __ec_align1;

struct ec_response_chg_limit_control {
	uint8_t max_percentage;
	uint8_t min_percentage;
} __ec_align1;

This host command sets, clears or query the charge limit.

Of particular note is the CHG_LIMIT_OVERRIDE flag: when this flag is set, the laptop will charge to 100% and stay there until it is unplugged.

Due the an issue, querying the charge limit will currently reset the override flag.

3E04 - Get Actual Fan RPM

Source

#define EC_CMD_PWM_GET_FAN_ACTUAL_RPM	0x3E04

struct ec_response_pwm_get_actual_fan_rpm {
	uint32_t rpm;
} __ec_align4;

Returns the current RPM of the first fan, calculated from the count in MCHP_TACH_0_STATUS.

3E05 - Set AP Reboot Delay

Source

#define EC_CMD_SET_AP_REBOOT_DELAY	0x3E05

struct ec_response_ap_reboot_delay {
	uint8_t delay;
} __ec_align1;

I believe this host command controls how long the EC will wait for the application processor to boot out of S5.

3E06 - ME Control

Source

#define EC_CMD_ME_CONTROL	0x3E06

enum ec_mecontrol_modes {
	ME_LOCK		= BIT(0),
	ME_UNLOCK	= BIT(1),
};

Locks or unlocks the ME region and immediately reboots the application processor.

3E07 - Custom Hello

Source

#define EC_CMD_CUSTOM_HELLO	0x3E07

Ensures that the EC is ready for pre-OS.

3E08 - Disable PS/2 Mouse Emulation

Source

#define EC_CMD_DISABLE_PS2_EMULATION 0x3E08

struct ec_params_ps2_emulation_control {
	uint8_t disable;
} __ec_align1;

This corresponds to the “Enable PS/2 Mouse Emulation” setting in Setup.

3E09 - Chassis Intrusion

#define EC_CMD_CHASSIS_INTRUSION 0x3E09
#define EC_PARAM_CHASSIS_INTRUSION_MAGIC 0xCE
#define EC_PARAM_CHASSIS_BBRAM_MAGIC 0xEC

struct ec_params_chassis_intrusion_control {
	uint8_t clear_magic;
	uint8_t clear_chassis_status;
} __ec_align1;

struct ec_response_chassis_intrusion_control {
	uint8_t chassis_ever_opened;
	uint8_t coin_batt_ever_remove;
	uint8_t total_open_count;
	uint8_t vtr_open_count;
} __ec_align1;

Query

When called with values 0x00 0x00, reports the chassis intrusion status.

Unlike 3E0F - Chassis Open Check, this host command reports all historical information on chassis intrusion.

Speculation: The UEFI could read this data and measure it into a TPM PCR; if so, any TPM data bound to that PCR would become unavailable if the chassis were opened. Kieran reports that it does not.

Reset

When called with clear_magic == EC_PARAM_CHASSIS_INTRUSION_MAGIC, this host command clears all chassis intrusion data (flag and counts.)

Deassert

When called with clear_chassis_status set, this host command clears only the chassis intrusion flag.

3E0A - BB Retimer Control

Source

#define EC_CMD_BB_RETIMER_CONTROL 0x3E0A

enum bb_retimer_control_mode {
	BB_ENTRY_FW_UPDATE_MODE = BIT(0),
	BB_EXIT_FW_UPDATE_MODE = BIT(1),
	BB_ENABLE_COMPLIANCE_MODE = BIT(2),
	BB_CHECK_STATUS	= BIT(7),
};

struct ec_params_bb_retimer_control_mode {
	uint8_t controller;
	uint8_t modes;
} __ec_align1;

struct ec_response_bb_retimer_control_mode {
	uint8_t status;
} __ec_align1;

3E0B - Diagnosis

Source

#define EC_CMD_DIAGNOSIS 0x3E0B

enum ec_params_diagnosis_code {
	CODE_DDR_TRAINING_START	= 1,
	CODE_DDR_TRAINING_FINISH = 2,
	CODE_DDR_FAIL = 3,
	CODE_NO_EDP = 4,
	CODE_PORT80_COMPLETE = 0xFF,
};

struct ec_params_diagnosis {
	uint8_t diagnosis_code;
} __ec_align1;

OEMBIOSSyncToECDxe invokes this host command with diagnosis_code CODE_NO_EDP if there is no display connected1. Presumably other parts of the UEFI firmware call this to indicate various hardware states.

3E0C - Update Keyboard Matrix

Source

#define EC_CMD_UPDATE_KEYBOARD_MATRIX 0x3E0C

struct keyboard_matrix_map {
	uint8_t row;
	uint8_t col;
	uint16_t scanset;
} __ec_align1;

struct ec_params_update_keyboard_matrix {
	uint32_t num_items;
	uint32_t write;
	struct keyboard_matrix_map scan_update[32];
} __ec_align1;

OEMBIOSSyncToECDxe invokes this host command at boot to sync up the Swap Ctrl/Fn setting.

Example (raw command)

Remapping 1, 12 (Left Ctrl) to SCANCODE_CAPSLOCK (0x058):

$ ectool raw 0x3E0C d1,d1,b1,bc,w58
Writing 3e0c [01 00 00 00 01 00 00 00 01 0C 58 00]
...reply snipped...
It works!

Remapping Caps Lock to Esc :

# Remap 4, 4 (Caps) to 0x0076 (ESC as defined in PC/AT)
$ ectool raw 0x3E0C d1,d1,b4,b4,76

See the Framework Key Matrix for all known key positions.

3E0D - vPro Control

Source

#define EC_CMD_VPRO_CONTROL	0x3E0D

enum ec_vrpo_control_modes {
	VPRO_OFF	= 0,
	VPRO_ON		= BIT(0),
};

struct ec_params_vpro_control {
	uint8_t vpro_mode;
} __ec_align1;

3E0E - Set Fingerprint LED Level Control

Source

#define EC_CMD_FP_LED_LEVEL_CONTROL 0x3E0E

struct ec_params_fp_led_control {
	uint8_t set_led_level;
	uint8_t get_led_level;
} __ec_align1;

enum fp_led_brightness_level {
	FP_LED_BRIGHTNESS_HIGH = 0,
	FP_LED_BRIGHTNESS_MEDIUM = 1,
	FP_LED_BRIGHTNESS_LOW = 2,
};

struct ec_response_fp_led_level {
	uint8_t level;
} __ec_align1;

3E0F - Chassis Open Check

Source

#define EC_CMD_CHASSIS_OPEN_CHECK 0x3E0F

struct ec_response_chassis_open_check {
	uint8_t status;
} __ec_align1;

The response to this host command is a byte signalling the immediate status of the chassis intrusion detection switch.

Unlike 3E09 - Chassis Intrusion, this host command only reports the current state of the chassis.

Flash Regions

3C000 - 3FFFF - Factory Info

This region contains information generated by or for the factory.

3C040 - 3C04F - First Boot Date

On boot, OEMBIOSSyncToECDxe reads 16 bytes from 3C040. If they’re uninitialized (0xFF), it populates them with the current day, month and year according to the RTC2. This only happens one time, so this flash region contains the date on which the laptop was first booted.

Observed Contents

0003C040: 0610 2100 0000 0000 0000 0000 0000 0000  ..!.............
           ^ ^  ^
	   | |  `-year (2 digits, BCD)
	   | `-month (BCD)
	   `-day (BCD)

Thanks to lbkNhubert and Jake_Bailey for confirming reports.

Other Exchanges

8048

During startup, OEMBIOSSyncToECDxe writes 0x8048 to port 0x802 and reads back one byte. The high bit in the address selects the second data bank3, leaving us with an offset of 0x48.

It mixes the response with a single byte from the UEFI Setup data (offset 0x994) and writes the resulting data back to bank 2 + 0x48.

uint8_t setup = /* ... read from Setup NVar */;
uint8_t val;
ec_transact(EC_TX_READ, 0x8048, &val, 1);
val = val & 0xfe | setup[0x99];
ec_transact(EC_TX_WRITE, 0x8048, &val, 1);

On my machine, this results in the following port writes:

0x802 <- 0x8048
0x804 -> v (= 0)
0x802 <- 0x8048
0x804 <- 0x29 (v & 0xfe | 0x29)

Platform Information

The EC is a Microchip MEC1521H.

Battery-Backed RAM

This chip offers a small amount of battery-backed RAM that is used to store various platform info and user configuration details across power state transitions.

Offset Used for…
0x1C Charge limit (max)
0x1D “Boot on AC Attach”
0x1E not used by Framework Laptop
0x1F Keyboard backlight (bits 0-6) + FN lock state (bit 7)
0x20 Chassis open count (total)
0x21 Chassis intrusion magic number 0xEC
0x22 Chassis opened while power off (VCI_IN2 signals)
0x23 Allow vPro Remote Wake flag
0x24 Chassis open flag
0x25 Power button brightness

Undocumented Features

The EC exposes a number of features that you do not need I/O port access or custom commands to access.

The Keyboard

The following undocumented Fn chords are recognized and handled by functional_hotkey and hotkey_special_key.

  • Fn+B - Ctrl+Break
  • Fn+P - Pause
  • Fn+K - Scroll Lock

Power Button

Holding the onboard power button for 10 seconds will force a battery disconnect.

Holding the fingerprint power button for 20 seconds will force an EC chip reset.


  1. Specifically, if EfiEdidDiscoveredProtocol doesn’t return any handles. ↩︎

  2. Data returned by the real-time clock is in binary-coded decimal (BCD) ↩︎

  3. The Microchip MEC172x data sheet calls this a REGION; section 16.10.4 pp. 280 ↩︎

  4. This offset isn’t referenced in the setup form, so it may not not be driven directly by user input on the Setup page. ↩︎