The Framework Laptop’s Embedded Controller (EC)
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:
- Write
0x0000 | 0x03
(offset 0, autoincrementing 32-bit I/O) to0x802
(16-bit). - Write data, 32 bits at a time, to
0x804
and0x806
.- For data longer than 32 bits, repeat writes to
0x804
and0x806
. - For data shorter than 32 bits, write it byte-by-byte into
0x804
,0x805
,0x806
and0x807
.
- For data longer than 32 bits, repeat writes to
- Write
0xDA
to0x204
.
Using fw-ectool
⌗
- 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
- Clone the repository.
make utils
. This will produce anectool
binary at$/build/bds/util/ectool
..../ectool version
- 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
orb1,w3
will write0x01 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.
- Download a release from the CrosEC releases page
- Disable Secure Boot and enable Test Signing
bcdedit /set {default} testsigning on
- You cannot toggle test signing while Secure Boot is enabled!
- Install
CrosEC.sys
usingdevcon
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.
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”⌗
#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⌗
#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⌗
#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⌗
#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⌗
#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⌗
#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⌗
#define EC_CMD_CUSTOM_HELLO 0x3E07
Ensures that the EC is ready for pre-OS.
3E08 - Disable PS/2 Mouse Emulation⌗
#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⌗
#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⌗
#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⌗
#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...
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⌗
#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⌗
#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⌗
#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 0x99
4) 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.
-
Specifically, if
EfiEdidDiscoveredProtocol
doesn’t return any handles. ↩︎ -
Data returned by the real-time clock is in binary-coded decimal (BCD) ↩︎
-
The Microchip MEC172x data sheet calls this a REGION; section 16.10.4 pp. 280 ↩︎
-
This offset isn’t referenced in the setup form, so it may not not be driven directly by user input on the Setup page. ↩︎