ec_fw: Initial commit for ec dtsi tool
- Migrated export-ec-devicetree-csv tool from arb cli
- Created pin maps for chips arbitrage tool supported
- Unit tests for new functions
- Example csv/generated dtsi
- Read me files for usage/building
- moved go.mod to root so both ap_fw and ec_fw can share it
BUG=b:427556454, b:475292981
TEST=Compared DTSI files generated by this tool for fatcat, nissa, rex
To output generated by arbitrage tool, verified only differences
resulting from new features/bugs in original arbitrage.
Change-Id: I6dc27ee29971ada122cc74c5be09e66c0cad3aa1
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/firmweaver/+/8090097
Commit-Queue: Kshitij Shah <tij@google.com>
Tested-by: Kshitij Shah <tij@google.com>
Reviewed-by: Karthik Ramasubramanian <kramasub@google.com>
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ca95764
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+# Binaries
+ec_fw/ec_fw
+ap_fw/gpio_tool
+
+# Go test binaries
+*.test
+
+# IDEs
+.vscode/
diff --git a/README.md b/README.md
index 7e35dd2..f97503a 100644
--- a/README.md
+++ b/README.md
@@ -18,8 +18,8 @@
* **Automatic Generation**: Generates firmware objects from hardware sheets.
* **Supported Outputs**:
- * EC Devicetree
- * AP GPIO Configuration
+ * EC Devicetree (`ec_fw`)
+ * AP GPIO Configuration (`ap_fw`)
## Getting Started
@@ -37,6 +37,20 @@
```
This will generate the `gpio_tool` executable in the `ap_fw` directory.
+### Building the EC Devicetree Generator
+
+The EC Devicetree generator is located in the `ec_fw` directory. See [`ec_fw/README.md`](ec_fw/README.md) for full details.
+
+1. Navigate to the `ec_fw` directory:
+ ```bash
+ cd ec_fw
+ ```
+2. Build the tool:
+ ```bash
+ go build -o ec_fw
+ ```
+ This will generate the `ec_fw` executable in the `ec_fw` directory.
+
## Usage
### Running the GPIO Tool
@@ -58,6 +72,28 @@
./ap_fw/gpio_tool --soc intel --csv ap_fw/test/ExampleGpio.csv --output ap_fw/test/gpio_config.c
```
+### Running the EC Devicetree Generator
+
+The `ec_fw` tool parses an EC pinout CSV spreadsheet and generates Zephyr devicetree overlays.
+
+```bash
+./ec_fw/ec_fw --chip <chip_name> --csv <path_to_csv> [--pin-col <variant>] [--output <path_to_output_dtsi>]
+```
+
+#### Example:
+To generate Nuvoton NPCX9 Devicetree overlay from the provided example CSV:
+```bash
+./ec_fw/ec_fw --chip npcx99 --csv ec_fw/test/ExampleEcPinout.csv --pin-col "NPCX99 (NPCX)" --output ec_fw/test/generated.dtsi
+```
+
+## Testing
+
+Run unit tests across all tools in the workspace:
+
+```bash
+go test -v ./...
+```
+
## Contributing
This project follows the standard Chromium OS contribution process.
diff --git a/ap_fw/go.mod b/ap_fw/go.mod
deleted file mode 100644
index fe80d3b..0000000
--- a/ap_fw/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module firmweaver/ap_fw
-
-go 1.26.4
diff --git a/ec_fw/README.md b/ec_fw/README.md
new file mode 100644
index 0000000..4bcbf4f
--- /dev/null
+++ b/ec_fw/README.md
@@ -0,0 +1,117 @@
+# EC Firmware Devicetree Generator (`ec_fw`)
+
+`ec_fw` is a tool for generating Zephyr EC Devicetree configuration files (`generated.dtsi`) for Chromium OS EC board bringup.
+
+It ingests hardware pinout CSV spreadsheets prepared by board design teams and outputs a self-contained Zephyr Devicetree overlay containing named GPIOs, ADC channels, PWM channels, I2C ports, TACH channels, and Keyboard matrix configurations.
+
+---
+
+## Features
+
+- **Variant Pinout Filtering (`--pin-col`)**: Selects variant-specific pin mapping columns directly from multi-variant board spreadsheets (e.g., `--pin-col="NPCX99 (NPCX)"` or `--pin-col="IT82002 (ITE)"`).
+- **Pin Maps**: Built-in chip pin maps are stored as embedded JSON data files (`data/chips/*.json`) and can be extended dynamically at runtime using `--chip-data-dir`.
+
+---
+
+## Supported EC Chips
+
+| Chip Family | Canonical Name | Supported Aliases / Variants | Pin Map Data File |
+|---|---|---|---|
+| **Nuvoton NPCX9** | `npcx99` | `npcx993`, `npcx996`, `npcx9` | [data/chips/npcx99.json](data/chips/npcx99.json) |
+| **ITE IT81302** | `it81302` | `it81302` | [data/chips/it81302.json](data/chips/it81302.json) |
+| **ITE IT81202** | `it81202` | `it81202` | [data/chips/it81202.json](data/chips/it81202.json) |
+| **ITE IT82002** | `it82002` | `it82002` | [data/chips/it82002.json](data/chips/it82002.json) |
+| **ITE IT82202** | `it82202` | `it82202ax` | [data/chips/it82202.json](data/chips/it82202.json) |
+
+---
+
+## CLI Reference
+
+```bash
+go run ./ec_fw --chip=<chip_name> --csv=<path_to_csv> [options]
+```
+
+### Options
+
+| Flag | Required | Description | Default |
+|---|---|---|---|
+| `--chip` | **Yes** | Target EC chip model or variant alias (e.g. `npcx993`, `it81302`, `it82202ax`). | `""` |
+| `--csv` | **Yes** | Path to the local CSV file containing the EC pinout table. | `""` |
+| `--pin-col` | No | Variant column header name in multi-variant spreadsheets (e.g. `"NPCX99 (NPCX)"`, `"IT82002 (ITE)"`). | `""` (defaults to `PinNumber` / `Pin`) |
+| `--chip-data-dir` | No | Path to an external directory containing custom chip JSON pin maps. | `""` |
+| `--output` | No | Path to write the output `generated.dtsi` file. | `stdout` |
+
+---
+
+## Usage Examples
+
+### 1. Generating Devicetree Using the Bundled Test Example
+To test generating a `generated.dtsi` file from the example test CSV included in `ec_fw/test/`:
+
+For NPCX (using `npcx99` chip):
+```bash
+go run ./ec_fw --chip=npcx99 \
+ --csv=ec_fw/test/ExampleEcPinout.csv \
+ --pin-col="NPCX99 (NPCX)" \
+ --output=ec_fw/test/generated_npcx.dtsi
+```
+
+For ITE (using `it82002` chip):
+```bash
+go run ./ec_fw --chip=it82002 \
+ --csv=ec_fw/test/ExampleEcPinout.csv \
+ --pin-col="IT82002 (ITE)" \
+ --output=ec_fw/test/generated_ite.dtsi
+```
+
+### 2. Generating Devicetree for Board Variants
+To generate a `generated.dtsi` file for the Fatcat **RVP** (NPCX chip):
+
+```bash
+go run ./ec_fw --chip=npcx99 \
+ --csv=/path/to/fatcat.csv \
+ --pin-col="NPCx9 RVP" \
+ --output=/path/to/platform/ec/zephyr/program/fatcat/generated_npcx.dtsi
+```
+
+To generate for the Fatcat **ITE** variant:
+
+```bash
+go run ./ec_fw --chip=it82002 \
+ --csv=/path/to/fatcat.csv \
+ --pin-col="IT82002 Ref" \
+ --output=/path/to/platform/ec/zephyr/program/fatcat/generated_ite.dtsi
+```
+
+### 3. Building and Running Binary
+Build the standalone `ec_fw` binary from the repository root:
+
+```bash
+go build -o ec_fw ./ec_fw
+./ec_fw --chip=it82202ax --csv=/path/to/hylia.csv --output=generated.dtsi
+```
+
+### 4. Adding Custom Chip Pin Maps
+To register custom EC chip models at runtime without modifying code:
+1. Create a JSON file (e.g. `custom_chip.json`):
+ ```json
+ {
+ "name": "custom_chip",
+ "aliases": ["custom_v1"],
+ "pin_map": {
+ "A1": "GPIOB4/I2C0_SDA0",
+ "A2": "GPIOB5/I2C0_SCL0"
+ }
+ }
+ ```
+2. Pass the directory containing your JSON file to `--chip-data-dir`:
+ ```bash
+ go run ./ec_fw --chip-data-dir=/path/to/custom_chips --chip=custom_chip --csv=board.csv
+ ```
+
+### 5. Running Tests
+Run unit tests across the `ec_fw` tool:
+
+```bash
+go test -v ./ec_fw/...
+```
diff --git a/ec_fw/chips.go b/ec_fw/chips.go
new file mode 100644
index 0000000..7007069
--- /dev/null
+++ b/ec_fw/chips.go
@@ -0,0 +1,304 @@
+// Copyright 2026 The ChromiumOS Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+import (
+ "embed"
+ "encoding/json"
+ "fmt"
+ "io/fs"
+ "maps"
+ "os"
+ "path/filepath"
+ "regexp"
+ "slices"
+ "strconv"
+ "strings"
+)
+
+//go:embed data/chips/*.json
+var embeddedChipsFS embed.FS
+
+var (
+ npcxRegex = regexp.MustCompile(`GP[IO]{0,2}([0-9A-F])([0-7])`)
+ iteRegex = regexp.MustCompile(`GP([A-Z])([0-7])`)
+ adcRegex = regexp.MustCompile(`ADC([0-9]+)`)
+ pwmRegex = regexp.MustCompile(`PWM([0-9]+)`)
+ npcxI2C = regexp.MustCompile(`I2C([0-9]+)_(SCL|SDA)`)
+ iteScl = regexp.MustCompile(`SMCLK([0-9]+[A-Z]*)`)
+ iteSda = regexp.MustCompile(`SMDAT([0-9]+[A-Z]*)`)
+ npcxTachRegex = regexp.MustCompile(`^T([A-Z])([0-9])$`)
+ iteTachRegex = regexp.MustCompile(`^TACH([0-9])([A-Z])$`)
+)
+
+type ChipData struct {
+ Name string `json:"name"`
+ Vendor string `json:"vendor"`
+ Aliases []string `json:"aliases"`
+ PinMap map[string]string `json:"pin_map"`
+}
+
+type Chip struct {
+ Name string
+ Vendor string
+ Aliases []string
+ PinMap map[string]string
+ PkgPins []string // List of sorted pin numbers (keys of PinMap)
+}
+
+func (c *Chip) IsNPCX() bool {
+ if c.Vendor != "" {
+ return strings.EqualFold(c.Vendor, "npcx")
+ }
+ return strings.HasPrefix(strings.ToLower(c.Name), "npcx")
+}
+
+func (c *Chip) IsITE() bool {
+ if c.Vendor != "" {
+ return strings.EqualFold(c.Vendor, "ite")
+ }
+ return strings.HasPrefix(strings.ToLower(c.Name), "it")
+}
+
+// ResolveGPIO returns the Zephyr GPIO port (e.g., "0", "e", "a") and pin number (0-7)
+func (c *Chip) ResolveGPIO(pinNum string) (string, int, error) {
+ pinDef, ok := c.PinMap[pinNum]
+ if !ok {
+ return "", 0, fmt.Errorf("pin %s not found in chip %s", pinNum, c.Name)
+ }
+
+ parts := strings.Split(pinDef, "/")
+ for _, part := range parts {
+ part = strings.TrimSpace(part)
+ if m := npcxRegex.FindStringSubmatch(part); m != nil {
+ port := strings.ToLower(m[1])
+ pin, err := strconv.Atoi(m[2])
+ if err != nil {
+ return "", 0, fmt.Errorf("invalid GPIO pin number %q in %s: %v", m[2], pinDef, err)
+ }
+ return port, pin, nil
+ }
+ }
+
+ for _, part := range parts {
+ part = strings.TrimSpace(part)
+ if m := iteRegex.FindStringSubmatch(part); m != nil {
+ port := strings.ToLower(m[1])
+ pin, err := strconv.Atoi(m[2])
+ if err != nil {
+ return "", 0, fmt.Errorf("invalid GPIO pin number %q in %s: %v", m[2], pinDef, err)
+ }
+ return port, pin, nil
+ }
+ }
+
+ if c.IsITE() {
+ for _, part := range parts {
+ part = strings.TrimSpace(part)
+ if strings.HasPrefix(part, "KSO") {
+ idxStr := strings.TrimPrefix(part, "KSO")
+ idx, err := strconv.Atoi(idxStr)
+ if err == nil {
+ if idx >= 0 && idx <= 7 {
+ return "ksol", idx, nil
+ } else if idx >= 8 && idx <= 15 {
+ return "ksoh", idx - 8, nil
+ }
+ }
+ }
+ }
+ }
+
+ return "", 0, fmt.Errorf("could not resolve GPIO for pin %s (%s)", pinNum, pinDef)
+}
+
+// ResolveADC returns the ADC channel (0-7) if the pin supports ADC
+func (c *Chip) ResolveADC(pinNum string) (int, error) {
+ pinDef, ok := c.PinMap[pinNum]
+ if !ok {
+ return 0, fmt.Errorf("pin %s not found in chip %s", pinNum, c.Name)
+ }
+
+ parts := strings.Split(pinDef, "/")
+ for _, part := range parts {
+ part = strings.TrimSpace(part)
+ if m := adcRegex.FindStringSubmatch(part); m != nil {
+ channel, err := strconv.Atoi(m[1])
+ if err != nil {
+ return 0, fmt.Errorf("invalid ADC channel %q in %s: %v", m[1], pinDef, err)
+ }
+ return channel, nil
+ }
+ }
+ return 0, fmt.Errorf("pin %s (%s) does not support ADC", pinNum, pinDef)
+}
+
+// ResolvePWM returns the PWM channel if the pin supports PWM
+func (c *Chip) ResolvePWM(pinNum string) (int, error) {
+ pinDef, ok := c.PinMap[pinNum]
+ if !ok {
+ return 0, fmt.Errorf("pin %s not found in chip %s", pinNum, c.Name)
+ }
+
+ parts := strings.Split(pinDef, "/")
+ for _, part := range parts {
+ part = strings.TrimSpace(part)
+ if m := pwmRegex.FindStringSubmatch(part); m != nil {
+ channel, err := strconv.Atoi(m[1])
+ if err != nil {
+ return 0, fmt.Errorf("invalid PWM channel %q in %s: %v", m[1], pinDef, err)
+ }
+ return channel, nil
+ }
+ }
+ return 0, fmt.Errorf("pin %s (%s) does not support PWM", pinNum, pinDef)
+}
+
+// ResolveI2C returns the I2C channel (as string, e.g. "0" or "2ALT") and whether it is SCL (true) or SDA (false)
+func (c *Chip) ResolveI2C(pinNum string) (string, bool, error) {
+ pinDef, ok := c.PinMap[pinNum]
+ if !ok {
+ return "", false, fmt.Errorf("pin %s not found in chip %s", pinNum, c.Name)
+ }
+
+ parts := strings.Split(pinDef, "/")
+ for _, part := range parts {
+ part = strings.TrimSpace(part)
+
+ if m := npcxI2C.FindStringSubmatch(part); m != nil {
+ isScl := m[2] == "SCL"
+ return m[1], isScl, nil
+ }
+
+ if m := iteScl.FindStringSubmatch(part); m != nil {
+ return m[1], true, nil
+ }
+ if m := iteSda.FindStringSubmatch(part); m != nil {
+ return m[1], false, nil
+ }
+ }
+ return "", false, fmt.Errorf("pin %s (%s) does not support I2C", pinNum, pinDef)
+}
+
+// ResolveTACH returns the TACH channel and port letter if the pin supports TACH.
+func (c *Chip) ResolveTACH(pinNum string) (int, string, error) {
+ pinDef, ok := c.PinMap[pinNum]
+ if !ok {
+ return 0, "", fmt.Errorf("pin %s not found in chip %s", pinNum, c.Name)
+ }
+
+ parts := strings.Split(pinDef, "/")
+ if c.IsNPCX() {
+ for _, part := range parts {
+ part = strings.TrimSpace(part)
+ if m := npcxTachRegex.FindStringSubmatch(part); m != nil {
+ portLetter := m[1]
+ chanNum, err := strconv.Atoi(m[2])
+ if err != nil {
+ return 0, "", fmt.Errorf("invalid TACH channel %q in %s: %v", m[2], pinDef, err)
+ }
+ return chanNum, portLetter, nil
+ }
+ }
+ } else if c.IsITE() {
+ for _, part := range parts {
+ part = strings.TrimSpace(part)
+ if m := iteTachRegex.FindStringSubmatch(part); m != nil {
+ chanNum, err := strconv.Atoi(m[1])
+ if err != nil {
+ return 0, "", fmt.Errorf("invalid TACH channel %q in %s: %v", m[1], pinDef, err)
+ }
+ portLetter := m[2]
+ return chanNum, portLetter, nil
+ }
+ }
+ }
+
+ return 0, "", fmt.Errorf("pin %s (%s) does not support TACH", pinNum, pinDef)
+}
+
+var chips = map[string]*Chip{}
+
+// GetChip returns the registered Chip by name or alias.
+func GetChip(name string) (*Chip, bool) {
+ chip, ok := chips[strings.ToLower(name)]
+ return chip, ok
+}
+
+// GetAvailableChips returns a sorted, comma-separated list of all registered chip names and aliases.
+func GetAvailableChips() string {
+ names := slices.Collect(maps.Keys(chips))
+ slices.Sort(names)
+ return strings.Join(names, ", ")
+}
+
+func RegisterChipData(data []byte) error {
+ var cd ChipData
+ if err := json.Unmarshal(data, &cd); err != nil {
+ return fmt.Errorf("failed to unmarshal chip json: %v", err)
+ }
+
+ var pins []string
+ for k := range cd.PinMap {
+ if k != "" {
+ pins = append(pins, k)
+ }
+ }
+ slices.Sort(pins)
+
+ chip := &Chip{
+ Name: cd.Name,
+ Vendor: cd.Vendor,
+ Aliases: cd.Aliases,
+ PinMap: cd.PinMap,
+ PkgPins: pins,
+ }
+
+ chips[strings.ToLower(cd.Name)] = chip
+ for _, alias := range cd.Aliases {
+ chips[strings.ToLower(alias)] = chip
+ }
+ return nil
+}
+
+// LoadExternalChipDir loads custom chip definitions from an external directory
+func LoadExternalChipDir(dirPath string) error {
+ entries, err := os.ReadDir(dirPath)
+ if err != nil {
+ return err
+ }
+ for _, entry := range entries {
+ if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".json") {
+ fullPath := filepath.Join(dirPath, entry.Name())
+ content, err := os.ReadFile(fullPath)
+ if err != nil {
+ return fmt.Errorf("failed to read chip file %s: %v", fullPath, err)
+ }
+ if err := RegisterChipData(content); err != nil {
+ return fmt.Errorf("error loading chip file %s: %v", fullPath, err)
+ }
+ }
+ }
+ return nil
+}
+
+func init() {
+ files, err := fs.ReadDir(embeddedChipsFS, "data/chips")
+ if err != nil {
+ panic(fmt.Sprintf("failed to read embedded chip dir: %v", err))
+ }
+
+ for _, file := range files {
+ if strings.HasSuffix(file.Name(), ".json") {
+ content, err := embeddedChipsFS.ReadFile(filepath.Join("data/chips", file.Name()))
+ if err != nil {
+ panic(fmt.Sprintf("failed to read embedded chip file %s: %v", file.Name(), err))
+ }
+ if err := RegisterChipData(content); err != nil {
+ panic(fmt.Sprintf("failed to register embedded chip file %s: %v", file.Name(), err))
+ }
+ }
+ }
+}
diff --git a/ec_fw/chips_test.go b/ec_fw/chips_test.go
new file mode 100644
index 0000000..3fca81d
--- /dev/null
+++ b/ec_fw/chips_test.go
@@ -0,0 +1,350 @@
+// Copyright 2026 The ChromiumOS Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func getChipOrFatal(t *testing.T, name string) *Chip {
+ c, ok := GetChip(name)
+ if !ok {
+ t.Fatalf("Chip %s not found", name)
+ }
+ return c
+}
+
+func TestNPCX99_ResolveGPIO(t *testing.T) {
+ chip := getChipOrFatal(t, "npcx99")
+
+ tests := []struct {
+ pinNum string
+ expectedPort string
+ expectedPin int
+ expectErr bool
+ }{
+ {"A2", "3", 1, false}, // KSI0/GPIO31/TRACEDATA3/GP_MOSI
+ {"B10", "0", 6, false}, // KSO11/GPIO06
+ {"D10", "c", 6, false}, // GPIOC6/SMI#
+ {"D1", "", 0, true}, // AVCC (not a GPIO)
+ {"Z99", "", 0, true}, // Pin not found
+ }
+
+ for _, tt := range tests {
+ port, pin, err := chip.ResolveGPIO(tt.pinNum)
+ if (err != nil) != tt.expectErr {
+ t.Errorf("ResolveGPIO(%s) error status mismatch: expected err=%t, got err=%v", tt.pinNum, tt.expectErr, err)
+ continue
+ }
+ if !tt.expectErr {
+ if port != tt.expectedPort || pin != tt.expectedPin {
+ t.Errorf("ResolveGPIO(%s) mismatch: expected port=%s pin=%d, got port=%s pin=%d", tt.pinNum, tt.expectedPort, tt.expectedPin, port, pin)
+ }
+ }
+ }
+}
+
+func TestITE82002_ResolveGPIO(t *testing.T) {
+ chip := getChipOrFatal(t, "it82002")
+
+ tests := []struct {
+ pinNum string
+ expectedPort string
+ expectedPin int
+ expectErr bool
+ }{
+ {"A10", "f", 2, false}, // PS2CLK1/DTR0#/SMINT8/GPF2
+ {"M6", "a", 2, false}, // PWM2/GPA2
+ {"N1", "d", 0, false}, // SMDAT2/RI1#/GPD0
+ {"A3", "b", 1, false}, // SOUT0/GPB1
+ {"A4", "b", 0, false}, // SIN0/GPB0
+ {"M8", "ksol", 0, false}, // KSO0/PD0
+ {"M13", "ksoh", 4, false}, // KSO12/SLCT
+ {"Z99", "", 0, true}, // Non-existent pin, should fail ResolveGPIO
+ }
+
+ for _, tt := range tests {
+ port, pin, err := chip.ResolveGPIO(tt.pinNum)
+ if (err != nil) != tt.expectErr {
+ t.Errorf("ResolveGPIO(%s) error status mismatch: expected err=%t, got err=%v", tt.pinNum, tt.expectErr, err)
+ continue
+ }
+ if !tt.expectErr {
+ if port != tt.expectedPort || pin != tt.expectedPin {
+ t.Errorf("ResolveGPIO(%s) mismatch: expected port=%s pin=%d, got port=%s pin=%d", tt.pinNum, tt.expectedPort, tt.expectedPin, port, pin)
+ }
+ }
+ }
+}
+
+func TestNPCX99_ResolveADC(t *testing.T) {
+ chip := getChipOrFatal(t, "npcx99")
+ tests := []struct {
+ pinNum string
+ expectedChan int
+ expectErr bool
+ }{
+ {"C2", 4, false}, // GPIO41/ADC4
+ {"D3", 3, false}, // GPIO42/ADC3/RI#
+ {"D10", 0, true}, // GPIOC6/SMI# (no ADC)
+ {"Z99", 0, true}, // Pin not found
+ }
+
+ for _, tt := range tests {
+ ch, err := chip.ResolveADC(tt.pinNum)
+ if (err != nil) != tt.expectErr {
+ t.Errorf("ResolveADC(%s) error status: expected err=%t, got %v", tt.pinNum, tt.expectErr, err)
+ continue
+ }
+ if !tt.expectErr && ch != tt.expectedChan {
+ t.Errorf("ResolveADC(%s) expected %d, got %d", tt.pinNum, tt.expectedChan, ch)
+ }
+ }
+}
+
+func TestITE82002_ResolvePWM(t *testing.T) {
+ chip := getChipOrFatal(t, "it82002")
+ tests := []struct {
+ pinNum string
+ expectedChan int
+ expectErr bool
+ }{
+ {"M6", 2, false}, // PWM2/GPA2
+ {"M7", 6, false}, // PWM6/SSCK/GPA6
+ {"M8", 0, true}, // KSO0/PD0 (no PWM)
+ {"Z99", 0, true}, // Pin not found
+ }
+
+ for _, tt := range tests {
+ ch, err := chip.ResolvePWM(tt.pinNum)
+ if (err != nil) != tt.expectErr {
+ t.Errorf("ResolvePWM(%s) error status: expected err=%t, got %v", tt.pinNum, tt.expectErr, err)
+ continue
+ }
+ if !tt.expectErr && ch != tt.expectedChan {
+ t.Errorf("ResolvePWM(%s) expected %d, got %d", tt.pinNum, tt.expectedChan, ch)
+ }
+ }
+}
+
+func TestChips_ResolveI2C(t *testing.T) {
+ npcx := getChipOrFatal(t, "npcx99")
+ ite := getChipOrFatal(t, "it82002")
+ it81202 := getChipOrFatal(t, "it81202")
+ it81302 := getChipOrFatal(t, "it81302")
+
+ tests := []struct {
+ chip *Chip
+ pinNum string
+ expectedChan string
+ expectedScl bool
+ expectErr bool
+ }{
+ // NPCX
+ {npcx, "B12", "0", false, false}, // GPIOB4/I2C0_SDA0
+ {npcx, "C12", "0", true, false}, // GPIOB5/I2C0_SCL0
+ {npcx, "A2", "", false, true}, // KSI0/GPIO31/TRACEDATA3/GP_MOSI (no I2C)
+ {npcx, "Z99", "", false, true}, // Pin not found
+
+ // ITE
+ {ite, "A5", "0", true, false}, // SMCLK0/GPB3
+ {ite, "A2", "0", false, false}, // SMDAT0/GPB4
+ {ite, "B1", "2ALT", true, false}, // SMCLK2ALT/PECI/GPF6
+ {ite, "C1", "2ALT", false, false}, // SMDAT2ALT/GPF7
+ {ite, "M8", "", false, true}, // KSO0/PD0 (no I2C)
+
+ // IT81202
+ {it81202, "M2", "2ALT", true, false}, // SMCLK2ALT
+ {it81202, "C1", "2ALT", false, false}, // SMDAT2ALT (prioritized over SMDAT2)
+ {it81202, "B1", "2", true, false}, // SMCLK2
+
+ // IT81302
+ {it81302, "N1", "2ALT", true, false}, // SMCLK2ALT
+ {it81302, "N15", "2ALT", false, false}, // SMDAT2ALT
+ }
+
+ for _, tt := range tests {
+ ch, scl, err := tt.chip.ResolveI2C(tt.pinNum)
+ if (err != nil) != tt.expectErr {
+ t.Errorf("[%s] ResolveI2C(%s) error status: expected err=%t, got %v", tt.chip.Name, tt.pinNum, tt.expectErr, err)
+ continue
+ }
+ if !tt.expectErr {
+ if ch != tt.expectedChan || scl != tt.expectedScl {
+ t.Errorf("[%s] ResolveI2C(%s) mismatch: expected chan=%s scl=%t, got chan=%s scl=%t", tt.chip.Name, tt.pinNum, tt.expectedChan, tt.expectedScl, ch, scl)
+ }
+ }
+ }
+}
+
+func TestChips_EmbeddedDataAndRegistration(t *testing.T) {
+ expectedChips := []string{"npcx99", "it81202", "it81302", "it82002", "it82202"}
+ for _, name := range expectedChips {
+ chip, ok := GetChip(name)
+ if !ok || chip == nil {
+ t.Errorf("Embedded chip %q missing from registered chips", name)
+ continue
+ }
+ if len(chip.PinMap) == 0 {
+ t.Errorf("Embedded chip %q has empty PinMap", name)
+ }
+ }
+}
+
+func TestITE_KSO_ResolveGPIO(t *testing.T) {
+ chip := &Chip{
+ Name: "it81302",
+ Vendor: "ite",
+ PinMap: map[string]string{
+ "P1": "KSO3",
+ "P2": "KSO12",
+ "P3": "KSO_INVALID",
+ "P4": "KSO16",
+ },
+ }
+
+ port, pin, err := chip.ResolveGPIO("P1")
+ if err != nil || port != "ksol" || pin != 3 {
+ t.Errorf("ResolveGPIO(P1 KSO3) expected port=ksol pin=3, got port=%s pin=%d err=%v", port, pin, err)
+ }
+
+ port, pin, err = chip.ResolveGPIO("P2")
+ if err != nil || port != "ksoh" || pin != 4 {
+ t.Errorf("ResolveGPIO(P2 KSO12) expected port=ksoh pin=4 (12-8), got port=%s pin=%d err=%v", port, pin, err)
+ }
+
+ _, _, err = chip.ResolveGPIO("P3")
+ if err == nil {
+ t.Error("Expected error for KSO_INVALID, got nil")
+ }
+
+ _, _, err = chip.ResolveGPIO("P4")
+ if err == nil {
+ t.Error("Expected error for KSO16, got nil")
+ }
+}
+
+func TestITE82202_FullResolution(t *testing.T) {
+ chip := getChipOrFatal(t, "it82202")
+
+ // ResolveGPIO
+ port, pin, err := chip.ResolveGPIO("M6") // PWM2/GPA2
+ if err != nil || port != "a" || pin != 2 {
+ t.Errorf("it82202 ResolveGPIO(M6) failed: port=%s pin=%d err=%v", port, pin, err)
+ }
+
+ // ResolvePWM
+ pwmChan, err := chip.ResolvePWM("M6")
+ if err != nil || pwmChan != 2 {
+ t.Errorf("it82202 ResolvePWM(M6) failed: pwm=%d err=%v", pwmChan, err)
+ }
+
+ // ResolveI2C
+ i2cChan, isScl, err := chip.ResolveI2C("B4") // SMCLK0/GPB3
+ if err != nil || i2cChan != "0" || !isScl {
+ t.Errorf("it82202 ResolveI2C(B4) failed: chan=%s isScl=%t err=%v", i2cChan, isScl, err)
+ }
+}
+
+func TestRegisterChipData_InvalidJSON(t *testing.T) {
+ err := RegisterChipData([]byte(`{invalid json`))
+ if err == nil {
+ t.Error("Expected error for invalid JSON, got nil")
+ }
+}
+
+func TestLoadExternalChipDir(t *testing.T) {
+ // Test non-existent directory
+ err := LoadExternalChipDir("non_existent_directory_xyz")
+ if err == nil {
+ t.Error("Expected error for non-existent directory, got nil")
+ }
+
+ // Test valid directory with a valid chip and an invalid chip
+ tmpDir, err := os.MkdirTemp("", "test_chips")
+ if err != nil {
+ t.Fatalf("Failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ // Write a valid chip JSON
+ validChipJSON := `{
+ "name": "temp-mock-chip",
+ "vendor": "mock",
+ "pin_map": {
+ "A1": "GPIOA1"
+ }
+ }`
+ err = os.WriteFile(filepath.Join(tmpDir, "temp_chip.json"), []byte(validChipJSON), 0644)
+ if err != nil {
+ t.Fatalf("Failed to write valid chip file: %v", err)
+ }
+
+ // Write an invalid chip JSON
+ invalidChipJSON := `{invalid json`
+ err = os.WriteFile(filepath.Join(tmpDir, "invalid_chip.json"), []byte(invalidChipJSON), 0644)
+ if err != nil {
+ t.Fatalf("Failed to write invalid chip file: %v", err)
+ }
+
+ // This should fail because of the invalid chip JSON
+ err = LoadExternalChipDir(tmpDir)
+ if err == nil {
+ t.Error("Expected error when loading directory with invalid JSON file, got nil")
+ }
+
+ // Remove the invalid chip and try again
+ err = os.Remove(filepath.Join(tmpDir, "invalid_chip.json"))
+ if err != nil {
+ t.Fatalf("Failed to remove invalid chip file: %v", err)
+ }
+
+ err = LoadExternalChipDir(tmpDir)
+ if err != nil {
+ t.Errorf("Expected success when loading valid chip directory, got error: %v", err)
+ }
+
+ // Verify the temp-mock-chip was registered
+ if _, ok := GetChip("temp-mock-chip"); !ok {
+ t.Error("temp-mock-chip was not registered")
+ }
+}
+
+func TestChips_ResolveTACH(t *testing.T) {
+ npcx := getChipOrFatal(t, "npcx99")
+ ite := getChipOrFatal(t, "it82002")
+
+ tests := []struct {
+ chip *Chip
+ pinNum string
+ expectedChan int
+ expectedPort string
+ expectErr bool
+ }{
+ // NPCX
+ {npcx, "E5", 1, "A", false}, // GPIO40/TA1
+ {npcx, "A2", 0, "", true}, // KSI0/GPIO31/TRACEDATA3/GP_MOSI (no TACH)
+ {npcx, "Z99", 0, "", true}, // Pin not found
+
+ // ITE
+ {ite, "M11", 0, "A", false}, // TACH0A/GPD6
+ {ite, "A10", 0, "", true}, // PS2CLK1/DTR0#/SMINT8/GPF2 (no TACH)
+ }
+
+ for _, tt := range tests {
+ ch, port, err := tt.chip.ResolveTACH(tt.pinNum)
+ if (err != nil) != tt.expectErr {
+ t.Errorf("%s ResolveTACH(%s) error status: expected err=%t, got %v", tt.chip.Name, tt.pinNum, tt.expectErr, err)
+ continue
+ }
+ if !tt.expectErr {
+ if ch != tt.expectedChan || port != tt.expectedPort {
+ t.Errorf("%s ResolveTACH(%s) expected chan=%d port=%s, got chan=%d port=%s", tt.chip.Name, tt.pinNum, tt.expectedChan, tt.expectedPort, ch, port)
+ }
+ }
+ }
+}
diff --git a/ec_fw/data/chips/it81202.json b/ec_fw/data/chips/it81202.json
new file mode 100644
index 0000000..88cab02
--- /dev/null
+++ b/ec_fw/data/chips/it81202.json
@@ -0,0 +1,134 @@
+{
+ "aliases": [],
+ "name": "it81202",
+ "pin_map": {
+ "A1": "RING_N/PWRFAIL_N/CK32OUT/LPCRST_N/GPB7",
+ "A10": "PS2CLK1/DTR0#/SMINT8/GPF2",
+ "A11": "PS2CLK0/TMB0/CEC/GPF0",
+ "A12": "EGCS_N/GPE2",
+ "A13": "EGAD/GPE1",
+ "A2": "SMDAT0/GPB4",
+ "A3": "SOUT0/GPB1",
+ "A4": "SIN0/GPB0",
+ "A5": "DTR1_N/SBUSY/GPG1/ID7",
+ "A6": "FMISO/GPG5",
+ "A7": "FSCE_N/GPG3",
+ "A8": "GPH5/ID5/DM",
+ "A9": "GPH3/ID3",
+ "B1": "SMCLK2/PECI/GPF6",
+ "B10": "PS2DAT1/RTS0_N/SMINT9/GPF3",
+ "B11": "PS2DAT0/TMB1/GPF1",
+ "B12": "EGCLK/GPE3",
+ "B13": "DAC4/DCD0_N/GPJ4",
+ "B2": "SMDAT1/GPC2",
+ "B3": "SMCLK1/GPC1",
+ "B4": "SMCLK0/GPB3",
+ "B5": "FSCK/GPG7",
+ "B6": "FMOSI/GPG4",
+ "B7": "GPH6/ID6/DP",
+ "B8": "GPH4/ID4",
+ "B9": "PS2DAT2/SMINT11/PD1CC2/GPF5",
+ "C1": "SMDAT2ALT/SMDAT2/PECIRQT_N/GPF7",
+ "C12": "DAC5/RIG0_N/GPJ5",
+ "C13": "DAC3/TACH1B/SMINT7/GPJ3",
+ "C2": "GPC4",
+ "D1": "CRX0/GPC0",
+ "D10": "VSTBY2",
+ "D12": "DAC2/TACH0B/SMINT6/GPJ2",
+ "D13": "SMINT5/GPJ1",
+ "D2": "CTX0/TMA0/GPB2",
+ "D4": "VSTBY1",
+ "D5": "VFSPI",
+ "D6": "DSR0_N/GPG6",
+ "D7": "CTX1/SOUT1/SMDAT3/PD2CC2/GPH2/ID2",
+ "D8": "CLKRUN_N/GPH0/ID0",
+ "D9": "PS2CLK2/SMINT10/PD1CC1/GPF4",
+ "E1": "GPC6",
+ "E10": "AVSS",
+ "E12": "TACH2/SMINT4/GPJ0",
+ "E13": "ADC7/CTS1_N/GPI7",
+ "E2": "PWRSW/GPE4",
+ "E4": "VSTBY3",
+ "E5": "VSS2",
+ "E6": "SSCE1_N/GPG0",
+ "E7": "SSCE0_N/GPG2",
+ "E8": "CRX1/SIN1/SMCLK3/PD2CC1/GPH1/ID1",
+ "E9": "AVCC",
+ "F1": "GA20/GPB5",
+ "F10": "ADC5/DCD1_N/GPI5",
+ "F12": "ADC6/DSR1_N/GPI6",
+ "F13": "ADC4/SMINT3/GPI4",
+ "F2": "CLK32K/GPJ6",
+ "F4": "VSS3",
+ "F5": "VSS4",
+ "F9": "ADC3/SMINT2/GPI3",
+ "G1": "GPJ7",
+ "G10": "ADC0/GPI0",
+ "G12": "ADC2/SMINT1/GPI2",
+ "G13": "ADC1/SMINT0/GPI1",
+ "G2": "ALERT_N/SERIRQ/GPM6",
+ "G4": "VSS5",
+ "G5": "VSS6",
+ "G9": "KSI7",
+ "H1": "ECS_N/LFRAME_N/GPM5",
+ "H10": "KSI5",
+ "H12": "KSI3/SLIN_N",
+ "H13": "KSI6",
+ "H2": "EIO3/LAD3/GPM3",
+ "H4": "KBRST_N/GPB6",
+ "H5": "VSS1",
+ "H9": "KSI4",
+ "J1": "EIO2/LAD2/GPM2",
+ "J10": "KSO17/SMISO/GPC5",
+ "J12": "KSI0/STB_N",
+ "J13": "KSI1/AFD_N",
+ "J2": "EIO1/LAD1/GPM1",
+ "J4": "VBAT",
+ "J5": "VCC",
+ "J6": "PWM5/SMDAT5/GPA5",
+ "J7": "KSO1/PD1",
+ "J8": "KSO5/PD5",
+ "J9": "KSI2/INIT_N",
+ "K1": "EIO0/LAD0/GPM0",
+ "K10": "VSTBY5",
+ "K12": "KSO15",
+ "K13": "KSO16/SMOSI/GPC3",
+ "K2": "ESCK/LPCCLK/GPM4",
+ "K4": "VSTBY4",
+ "K5": "VCORE",
+ "K6": "PWM4/SMCLK5/GPA4",
+ "K7": "PWM7/RIG1_N/GPA7",
+ "K8": "KSO4/PD4",
+ "K9": "KSO9/BUSY",
+ "L1": "WRST_N",
+ "L12": "KSO13",
+ "L13": "KSO14",
+ "L2": "ECSMI_N/GPD4",
+ "M1": "LPCPD_N/GPE6",
+ "M10": "KSO7/PD7",
+ "M11": "TACH0A/GPD6",
+ "M12": "TACH1A/TMA1/GPD7",
+ "M13": "KSO12/SLCT",
+ "M2": "PWUREQ_N/BBO/SMCLK2ALT/GPC7",
+ "M3": "L80LLAT/SMDAT4/GPE7",
+ "M4": "ERST_N/LPCRST_N/GPD2",
+ "M5": "PWM0/GPA0",
+ "M6": "PWM2/GPA2",
+ "M7": "PWM6/SSCK/GPA6",
+ "M8": "KSO0/PD0",
+ "M9": "KSO3/PD3",
+ "N1": "RI1_N/GPD0",
+ "N10": "KSO6/PD6",
+ "N11": "KSO8/ACK_N",
+ "N12": "KSO10/PE",
+ "N13": "KSO11/ERR_N",
+ "N2": "L80HLAT/BAO/SMCLK4/GPE0",
+ "N3": "RI2_N/GPD1",
+ "N4": "ECSCI_N/GPD3",
+ "N5": "PWM1/GPA1",
+ "N6": "PWM3/GPA3",
+ "N7": "GINT/CTS0_N/GPD5",
+ "N8": "RTS1_N/GPE5",
+ "N9": "KSO2/PD2"
+ }
+}
diff --git a/ec_fw/data/chips/it81302.json b/ec_fw/data/chips/it81302.json
new file mode 100644
index 0000000..6050e0b
--- /dev/null
+++ b/ec_fw/data/chips/it81302.json
@@ -0,0 +1,150 @@
+{
+ "aliases": [],
+ "name": "it81302",
+ "pin_map": {
+ "A1": "GPL5",
+ "A10": "SMINT11/PD1CC2/GPF5",
+ "A11": "RTS0#/SMINT9/GPF3",
+ "A12": "GPF1",
+ "A13": "ADC13/GPL0",
+ "A14": "ADC14/GPL1",
+ "A15": "ADC15/GPL2",
+ "A2": "GPL4",
+ "A3": "SMDAT1/GPC2",
+ "A4": "SMCLK1/GPC1",
+ "A5": "SMCLK0/GPB3",
+ "A6": "FSCK/GPG7",
+ "A7": "FMOSI/GPG4",
+ "A8": "GPH6/ID6",
+ "A9": "GPH4/ID4",
+ "B1": "GPL6",
+ "B10": "GPH3/ID3",
+ "B11": "DTR0#/SMINT8/GPF2",
+ "B12": "CEC/GPF0",
+ "B13": "GPE2",
+ "B14": "GPE1",
+ "B15": "ADC16/GPL3",
+ "B2": "RING#/PWRFAIL#/CK32KOUT/LPCRST#/GPB7",
+ "B3": "SMDAT0/GPB4",
+ "B4": "SOUT0/GPB1",
+ "B5": "SIN0/GPB0",
+ "B6": "DTR1#/GPG1/ID7",
+ "B7": "FMISO/GPG5",
+ "B8": "FSCE#/GPG3",
+ "B9": "GPH5/ID5",
+ "C1": "GPL7",
+ "C14": "DCD0#/GPJ4",
+ "C15": "GPE3",
+ "C2": "SMCLK2/PECI/GPF6",
+ "D1": "GPC4",
+ "D14": "TACH1B/SMINT7/GPJ3",
+ "D15": "RIG0#/GPJ5",
+ "D2": "SMDAT2/PECIRQT#/GPF7",
+ "E1": "GPB2",
+ "E10": "SMINT10/PD1CC1/GPF4",
+ "E11": "VSTBY2",
+ "E14": "SMINT5/GPJ1",
+ "E15": "TACH0B/SMINT6/GPJ2",
+ "E2": "GPC0",
+ "E5": "VSTBY1",
+ "E6": "VFSPI",
+ "E7": "DSR0#/GPG6",
+ "E8": "SOUT1/SMDAT3/PD2CC2/GPH2/ID2",
+ "E9": "CLKRUN#/GPH0/ID0",
+ "F1": "PWRSW/GPE4",
+ "F10": "AVCC",
+ "F11": "AVSS",
+ "F14": "ADC7/CTS1#/GPI7",
+ "F15": "TACH2/SMINT4/GPJ0",
+ "F2": "GPC6",
+ "F5": "VSTBY3",
+ "F6": "VSS1",
+ "F7": "SSCE1#/GPG0",
+ "F8": "SSCE0#/GPG2",
+ "F9": "SIN1/SMCLK3/PD2CC1/GPH1/ID1",
+ "G1": "CK32K/GPJ6",
+ "G10": "ADC3/SMINT2/GPI3",
+ "G11": "ADC5/DCD1#/GPI5",
+ "G14": "ADC4/SMINT3/GPI4",
+ "G15": "ADC6/DSR1#/GPI6",
+ "G2": "GA20/GPB5",
+ "G5": "VSS2",
+ "G6": "VSS3",
+ "H1": "ALERT#/SERIRQ/GPM6",
+ "H10": "KSI7",
+ "H11": "ADC0/GPI0",
+ "H14": "ADC1/SMINT0/GPI1",
+ "H15": "ADC2/SMINT1/GPI2",
+ "H2": "GPJ7",
+ "H5": "VSS4",
+ "H6": "VSS5",
+ "J1": "EIO3/LAD3/GPM3",
+ "J10": "KSI4",
+ "J11": "KSI5",
+ "J14": "KSI6",
+ "J15": "KSI3/SLIN#",
+ "J2": "ECS#/LFRAME#/GPM5",
+ "J5": "KBRST#/GPB6",
+ "J6": "VSS6",
+ "K1": "EIO1/LAD1/GPM1",
+ "K10": "KSI2/INIT#",
+ "K11": "KSO17/SMISO/GPC5",
+ "K14": "KSI1/AFD#",
+ "K15": "KSI0/STB#",
+ "K2": "EIO2/LAD2/GPM2",
+ "K5": "VBAT",
+ "K6": "VCC",
+ "K7": "PWM5/SMDAT5/GPA5",
+ "K8": "KSO1/PD1",
+ "K9": "KSO5/PD5",
+ "L1": "ESCK/LPCCLK/GPM4",
+ "L10": "KSO9/BUSY",
+ "L11": "VSTBY5",
+ "L14": "KSO16/SMOSI/GPC3",
+ "L15": "KSO15",
+ "L2": "EIO0/LAD0/GPM0",
+ "L5": "VSTBY4",
+ "L6": "VCORE",
+ "L7": "PWM4/SMCLK5/GPA4",
+ "L8": "PWM7/RIG1#/GPA7",
+ "L9": "KSO4/PD4",
+ "M1": "ECSMI#/GPD4",
+ "M14": "KSO14",
+ "M15": "KSO13",
+ "M2": "WRST#",
+ "N1": "PWUREQ#/BBO/SMCLK2ALT/GPC7",
+ "N14": "KSO12/SLCT",
+ "N15": "WUI14/GPK6/SMDAT2ALT",
+ "N2": "LPCPD#/GPE6",
+ "P1": "WUI8/GPK0",
+ "P10": "KSO2/PD2",
+ "P11": "KSO6/PD6",
+ "P12": "KSO8/ACK#",
+ "P13": "KSO10/PE",
+ "P14": "KSO11/ERR#",
+ "P15": "WUI15/GPK7",
+ "P2": "RI1#/GPD0",
+ "P3": "L80HLAT/BAO/SMCLK4/GPE0",
+ "P4": "RI2#/GPD1",
+ "P5": "ECSCI#/GPD3",
+ "P6": "PWM1/GPA1",
+ "P7": "PWM3/GPA3",
+ "P8": "GINT/CTS0#/GPD5",
+ "P9": "RTS1#/GPE5",
+ "R1": "WUI9/GPK1",
+ "R10": "KSO3/PD3",
+ "R11": "KSO7/PD7",
+ "R12": "TACH0/GPD6",
+ "R13": "TACH1A/GPD7",
+ "R14": "WUI13/GPK5",
+ "R15": "WUI12/GPK4",
+ "R2": "WUI10/GPK2",
+ "R3": "WUI11/GPK3",
+ "R4": "L80LLAT/SMDAT4/GPE7",
+ "R5": "ERST#/LPCRST#/GPD2",
+ "R6": "PWM0/GPA0",
+ "R7": "PWM2/GPA2",
+ "R8": "PWM6/SSCK/GPA6",
+ "R9": "KSO0/PD0"
+ }
+}
diff --git a/ec_fw/data/chips/it82002.json b/ec_fw/data/chips/it82002.json
new file mode 100644
index 0000000..41b8b36
--- /dev/null
+++ b/ec_fw/data/chips/it82002.json
@@ -0,0 +1,134 @@
+{
+ "aliases": [],
+ "name": "it82002",
+ "pin_map": {
+ "A1": "VBAT3V",
+ "A10": "PS2CLK1/DTR0#/GPF2",
+ "A11": "SIN1/PS2CLK0/CEC/GPF0",
+ "A12": "SMDAT5/GPE2",
+ "A13": "SMCLK5/GPE1",
+ "A2": "SMDAT0/GPB4",
+ "A3": "SOUT0/GPB1",
+ "A4": "SIN0/GPB0",
+ "A5": "SMCLK0/GPB3",
+ "A6": "FMISO/GPG5",
+ "A7": "FSCE#/GPG3",
+ "A8": "GPH5/DM",
+ "A9": "GPH3",
+ "B1": "SMCLK2ALT/PECI/GPF6",
+ "B10": "PS2DAT1/RTS0#/ GPF3",
+ "B11": "SOUT1/PS2DAT0/GPF1",
+ "B12": "GPE3",
+ "B13": "DCD0#/GPJ4",
+ "B2": "SMDAT1/GPC2",
+ "B3": "SMCLK1/GPC1",
+ "B4": "PWRSW/GPE4",
+ "B5": "FSCK/GPG7",
+ "B6": "FMOSI/GPG4",
+ "B7": "GPH6/DP",
+ "B8": "GPH4",
+ "B9": "PS2DAT2/GPF5",
+ "C1": "SMDAT2ALT/GPF7",
+ "C12": "RIG0#/GPJ5",
+ "C13": "DAC1/TACH1B/GPJ3",
+ "C2": "GPC4",
+ "D1": "DSR0#/GPG6",
+ "D10": "VSTBY'<3>",
+ "D12": "V3318",
+ "D13": "GPJ1",
+ "D2": "SMCLK3/GPB2",
+ "D4": "VSTBY'<1>",
+ "D5": "VSTBY'<2>",
+ "D6": "VSS'<1>",
+ "D7": "SMDAT3ALT/GPH2",
+ "D8": "CLKRUN#/GPH0",
+ "D9": "PS2CLK2/GPF4",
+ "E1": "GPC6/SIO3",
+ "E10": "AVSS",
+ "E12": "TACH2/GPJ0",
+ "E13": "ADC7/CTS1#/GPI7",
+ "E2": "SSCE1#/GPG0",
+ "E4": "VSTBY'<6>",
+ "E5": "GPC0/SIO2",
+ "E6": "VFSPI",
+ "E7": "SSCE0#/GPG2",
+ "E8": "SMCLK3ALT/GPH1",
+ "E9": "AVCC",
+ "F1": "SMDAT3/GA20/GPB5",
+ "F10": "ADC5/DCD1#/GPI5",
+ "F12": "ADC6/DSR1#/GPI6",
+ "F13": "ADC4/GPI4",
+ "F2": "TACH1B/SDIO3/SMDAT8/GPJ6",
+ "F4": "DTR1#/GPG1",
+ "F5": "VSS'<2>",
+ "F9": "ADC3/GPI3",
+ "G1": "SSCE0#/ALERT#/GPJ7",
+ "G10": "ADC0/GPI0",
+ "G12": "ADC2/GPI2",
+ "G13": "ADC1/GPI1",
+ "G2": "ALERT#/SERIRQ/GPM6",
+ "G4": "VSS'<3>",
+ "G5": "VSS'<4>",
+ "G9": "KSI7",
+ "H1": "ECS#/LFRAME#/GPM5",
+ "H10": "KSI5",
+ "H12": "KSI3/SLIN#",
+ "H13": "KSI6",
+ "H2": "EIO3/LAD3/GPM3",
+ "H4": "KBRST#/GPB6",
+ "H5": "VSS'<5>",
+ "H9": "KSI4",
+ "J1": "EIO2/LAD2/GPM2",
+ "J10": "KSO17/SMISO/GPC5",
+ "J12": "KSI0/STB#",
+ "J13": "KSI1/AFD#",
+ "J2": "EIO1/LAD1/GPM1",
+ "J4": "VCORE2",
+ "J5": "VCC",
+ "J6": "PWM5/SMDAT5ALT/GPA5",
+ "J7": "KSO1/PD1",
+ "J8": "KSO5/PD5",
+ "J9": "KSI2/INIT#",
+ "K1": "EIO0/LAD0/GPM0",
+ "K10": "VSTBY'<5>",
+ "K12": "KSO15",
+ "K13": "KSO16/SMOSI/GPC3",
+ "K2": "ESCK/LPCCLK/GPM4",
+ "K4": "VSTBY'<4>",
+ "K5": "VCORE",
+ "K6": "PWM4/SMCLK5ALT/GPA4",
+ "K7": "PWM7/RIG1#/GPA7",
+ "K8": "KSO4/PD4",
+ "K9": "KSO9/BUSY",
+ "L1": "WRST#",
+ "L12": "KSO13",
+ "L13": "KSO14",
+ "L2": "ECSMI#/GPD4",
+ "M1": "LPCPD#/GPE6",
+ "M10": "KSO7/PD7",
+ "M11": "TACH0A/GPD6",
+ "M12": "TACH1A/GPD7",
+ "M13": "KSO12/SLCT",
+ "M2": "PWUREQ#/BBO/SMCLK2/GPC7",
+ "M3": "L80LLAT/SMDAT4/GPE7",
+ "M4": "ERST#/LPCRST#/GPD2",
+ "M5": "PWM0/GPA0",
+ "M6": "PWM2/GPA2",
+ "M7": "PWM6/SSCK/GPA6",
+ "M8": "KSO0/PD0",
+ "M9": "KSO3/PD3",
+ "N1": "SMDAT2/RI1#/GPD0",
+ "N10": "KSO6/PD6",
+ "N11": "KSO8/ACK#",
+ "N12": "KSO10/PE",
+ "N13": "KSO11/ERR#",
+ "N2": "L80HLAT/BAO/SMCLK4/GPE0",
+ "N3": "RI2#/GPD1",
+ "N4": "ECSCI#/GPD3",
+ "N5": "PWM1/GPA1",
+ "N6": "PWM3/GPA3",
+ "N7": "GINT/CTS0#/GPD5",
+ "N8": "RTS1#/GPE5",
+ "N9": "KSO2/PD2"
+ }
+}
diff --git a/ec_fw/data/chips/it82202.json b/ec_fw/data/chips/it82202.json
new file mode 100644
index 0000000..32a2bcc
--- /dev/null
+++ b/ec_fw/data/chips/it82202.json
@@ -0,0 +1,136 @@
+{
+ "aliases": [
+ "it82202ax"
+ ],
+ "name": "it82202",
+ "pin_map": {
+ "A1": "VCORE2",
+ "A10": "PS2CLK1/DTR0#/SMINT8/GPF2",
+ "A11": "PS2CLK0/CEC/GPF0",
+ "A12": "SMDAT5/GPE2",
+ "A13": "SMCLK5/GPE1",
+ "A2": "SMDAT0/GPB4",
+ "A3": "SOUT0/GPB1",
+ "A4": "SIN0/GPB0",
+ "A5": "DTR1#/GPG1/ID7 ",
+ "A6": "FMISO/GPG5",
+ "A7": "FSCE_N/GPG3",
+ "A8": "GPH5/ID5/DM",
+ "A9": "GPH3/ID3",
+ "B1": "SMCLK2ALT/PECI/GPF6",
+ "B10": "PS2DAT1/RTS0#/SMINT9/GPF3",
+ "B11": "PS2DAT0/GPF1",
+ "B12": "GPE3",
+ "B13": "DCD0#/GPJ4",
+ "B2": "SMDAT1/GPC2",
+ "B3": "SMCLK1/GPC1",
+ "B4": "SMCLK0/GPB3",
+ "B5": "FSCK/GPG7",
+ "B6": "FMOSI/GPG4",
+ "B7": "GPH6/ID6/DP",
+ "B8": "GPH4/ID4",
+ "B9": "PS2DAT2/SMINT11/PD1CC2/GPF5",
+ "C1": "SMDAT2ALT/GPF7 ",
+ "C12": "RIG0#/GPJ5",
+ "C13": "DAC1/TACH1B/SMINT7/GPJ3",
+ "C2": "GPC4",
+ "D1": "GPC0",
+ "D10": "VSTBY3",
+ "D12": "DAC0/TACH0B/SMINT6/GPJ2",
+ "D13": "SMINT5/GPJ1",
+ "D2": "SMCLK3/GPB2 ",
+ "D4": "VSTBY",
+ "D5": "VFSPI",
+ "D6": "DSR0_N/GPG6",
+ "D7": "SOUT1/SMDAT3ALT/PD2CC2/GPH2/ID2",
+ "D8": "CLKRUN_N/GPH0/ID0",
+ "D9": "PS2CLK2/SMINT10/PD1CC1/GPF4",
+ "E1": "GPC6",
+ "E10": "AVSS",
+ "E12": "TACH2/SMINT4/GPJ0",
+ "E13": "ADC7/CTS1_N/GPI7",
+ "E2": "PWRSW/GPE4",
+ "E4": "VSTBY2",
+ "E5": "VSS",
+ "E6": "SSCE1_N/GPG0",
+ "E7": "SSCE0_N/GPG2",
+ "E8": "SIN1/SMCLK3ALT/PD2CC1/GPH1/ID1",
+ "E9": "AVCC",
+ "F1": "SMDAT3/GA20/GPB5",
+ "F10": "ADC5/DCD1_N/GPI5",
+ "F12": "ADC6/DSR1_N/GPI6",
+ "F13": "ADC4/SMINT3/GPI4",
+ "F2": "CK32K",
+ "F4": "VSS",
+ "F5": "VSS",
+ "F9": "ADC3/SMINT2/GPI3",
+ "G1": "CK32KE ",
+ "G10": "ADC0/GPI0",
+ "G12": "ADC2/SMINT1/GPI2",
+ "G13": "ADC1/SMINT0/GPI1",
+ "G2": "ALERT_N/SERIRQ/GPM6",
+ "G4": "VSS",
+ "G5": "VSS",
+ "G9": "KSI7",
+ "H1": "ECS_N/LFRAME_N/GPM5",
+ "H10": "KSI5",
+ "H12": "KSI3/SLIN_N",
+ "H13": "KSI6",
+ "H2": "EIO3/LAD3/GPM3",
+ "H4": "KBRST_N/GPB6",
+ "H5": "VSS",
+ "H9": "KSI4",
+ "J1": "EIO2/LAD2/GPM2",
+ "J10": "KSO17/SMISO/GPC5",
+ "J12": "KSI0/STB_N",
+ "J13": "KSI1/AFD_N",
+ "J2": "EIO1/LAD1/GPM1",
+ "J4": "VBAT",
+ "J5": "VCC",
+ "J6": "PWM5/SMDAT5ALT/GPA5",
+ "J7": "KSO1/PD1",
+ "J8": "KSO5/PD5",
+ "J9": "KSI2/INIT_N",
+ "K1": "EIO0/LAD0/GPM0",
+ "K10": "VSTBY",
+ "K12": "KSO15",
+ "K13": "KSO16/SMOSI/GPC3",
+ "K2": "ESCK/LPCCLK/GPM4",
+ "K4": "VSTBY",
+ "K5": "VCORE",
+ "K6": "PWM4/SMCLK5ALT/GPA4",
+ "K7": "PWM7/RIG1_N/GPA7",
+ "K8": "KSO4/PD4",
+ "K9": "KSO9/BUSY",
+ "L1": "WRST_N",
+ "L12": "KSO13",
+ "L13": "KSO14",
+ "L2": "ECSMI_N/GPD4",
+ "M1": "LPCPD_N/GPE6",
+ "M10": "KSO7/PD7",
+ "M11": "TACH0A/GPD6",
+ "M12": "TACH1A/GPD7",
+ "M13": "KSO12/SLCT",
+ "M2": "PWUREQ#/BBO/SMCLK2/GPC7",
+ "M3": "L80LLAT/SMDAT4/GPE7",
+ "M4": "ERST_N/LPCRST_N/GPD2",
+ "M5": "PWM0/GPA0",
+ "M6": "PWM2/GPA2",
+ "M7": "PWM6/SSCK/GPA6",
+ "M8": "KSO0/PD0",
+ "M9": "KSO3/PD3",
+ "N1": "RI1#/SMDAT2/GPD0",
+ "N10": "KSO6/PD6",
+ "N11": "KSO8/ACK_N",
+ "N12": "KSO10/PE",
+ "N13": "KSO11/ERR_N",
+ "N2": "L80HLAT/BAO/SMCLK4/GPE0",
+ "N3": "RI2_N/GPD1",
+ "N4": "ECSCI_N/GPD3",
+ "N5": "PWM1/GPA1",
+ "N6": "PWM3/GPA3",
+ "N7": "GINT/CTS0_N/GPD5",
+ "N8": "RTS1_N/GPE5",
+ "N9": "KSO2/PD2"
+ }
+}
diff --git a/ec_fw/data/chips/npcx99.json b/ec_fw/data/chips/npcx99.json
new file mode 100644
index 0000000..b808a2c
--- /dev/null
+++ b/ec_fw/data/chips/npcx99.json
@@ -0,0 +1,155 @@
+{
+ "aliases": [
+ "npcx993",
+ "npcx996",
+ "npcx99*"
+ ],
+ "name": "npcx99",
+ "pin_map": {
+ "": "",
+ "A1": "VSS_1",
+ "A10": "GPIOD5/INTRUDER#",
+ "A11": "GPIOE2",
+ "A12": "GPIOE5",
+ "A2": "KSI0/GPIO31/TRACEDATA3/GP_MOSI",
+ "A3": "KSI1/GPIO30/TRACEDATA2/GP_CS#",
+ "A4": "KSI2/GPIO27/TRACEDATA1",
+ "A5": "VSS_2",
+ "A6": "CAP (Vcorf)",
+ "A7": "VCC1_1",
+ "A8": "VSS_3",
+ "A9": "GPIOD4/CR_SIN3",
+ "B1": "VCC1_2",
+ "B10": "KSO11/GPIO06",
+ "B11": "KSO10/GPIO07",
+ "B12": "GPIOB4/I2C0_SDA0",
+ "B2": "GPIO34/PS2_DAT2/ADC6",
+ "B3": "KSI3/GPIO26/TRACEDATA0",
+ "B4": "KSI4/GPIO25/TRACECLK/GP_SCLK",
+ "B5": "KSO00/GPIO21/JTAG_TCK0_SWCLK0",
+ "B6": "KSO01/GPIO20/JTAG_TMS0_SWIO0",
+ "B7": "KSO02/GPIO17/JTAG_TDI0",
+ "B8": "KSO03/GPIO16/JTAG_TDO0_SWO0",
+ "B9": "KSO07/GPO12/JEN#",
+ "C1": "GPIO37/PS2_CLK2/ADC5",
+ "C10": "KSO09/GPIO10/CR_SIN1",
+ "C11": "KSO12/GPIO05",
+ "C12": "GPIOB5/I2C0_SCL0",
+ "C2": "GPIO41/ADC4",
+ "C3": "KSI5/GPIO24/GP_MISO",
+ "C4": "KSI6/GPIO23/S_SBUB",
+ "C5": "KSI7/GPIO22/S_SBUA",
+ "C6": "KSO05/GPIO14",
+ "C7": "KSO04/GPIO15/XNOR",
+ "C8": "KSO06/GPO13/GP_SEL#",
+ "C9": "KSO08/GPIO11/CR_SOUT1",
+ "D1": "AVCC",
+ "D10": "GPIOC6/SMI#",
+ "D11": "KSO13/GPIO04",
+ "D12": "VCC1_3",
+ "D2": "GPIOF0/ADC9",
+ "D3": "GPIO42/ADC3/RI#",
+ "D4": "GPIO36/RTS#/I2C5_SDA0",
+ "D5": "GPIO33/CTS#/I2C5_SCL0",
+ "D6": "KSO14/GPIO82",
+ "D7": "KSO15/GPIO83",
+ "D8": "KSO17/GPIOB1/CR_SIN4",
+ "D9": "KSO16/GPIO03",
+ "E1": "AVSS",
+ "E10": "GPIOD3/TB1",
+ "E11": "GPIO93/TA1/F_DIO2",
+ "E12": "VSS_4",
+ "E2": "GPIO43/ADC2",
+ "E3": "GPIO44/ADC1",
+ "E4": "GPO32/TRIS#",
+ "E5": "GPIO40/TA1",
+ "E6": "GPIO01/PSL_IN3#",
+ "E7": "PSL_IN2#/GPIO00",
+ "E8": "GPIOF5/I2C5_SCL1",
+ "E9": "GPIOF4/I2C5_SDA1",
+ "F1": "VHIF",
+ "F10": "GPIOC7/DTR#_BOUT/ADC11",
+ "F11": "GPIOA6/PS2_CLK3/TA2/F_CS1#",
+ "F12": "F_SCLK/GPIOA2",
+ "F2": "GPIO45/ADC0",
+ "F3": "GPIOE1/ADC7",
+ "F4": "GPIOE0/ADC10",
+ "F5": "GPIOF3/I2C4_SCL1",
+ "F6": "GPIOF2/I2C4_SDA1",
+ "F7": "GPIO02/PSL_IN4#",
+ "F8": "GPIOD1/I2C3_SCL0",
+ "F9": "GPIOD0/I2C3_SDA0",
+ "G1": "VSS_5",
+ "G10": "GPIO50",
+ "G11": "F_CS0#/GPIOA0",
+ "G12": "F_DIO1/GPIO96",
+ "G2": "GPIO66",
+ "G3": "GPIOF1/ADC8",
+ "G4": "GPIO64/CR_SIN1",
+ "G5": "GPIO73/TA2",
+ "G6": "GPIO60/PWM7",
+ "G7": "PSL_IN1#/GPIOD2",
+ "G8": "GPIOC4/PWM2",
+ "G9": "GPIOC3/PWM0",
+ "H1": "eSPI_IO0/GPIO46/F_DIO0/SHI_SDI",
+ "H10": "GPIOC2/PWM1/I2C6_SCL0",
+ "H11": "F_DIO0/GPIOA4/TB1",
+ "H12": "VSPI",
+ "H2": "GPIO62/PS2_CLK1",
+ "H3": "GPOD6/CR_SOUT3/SHDF_ESPI#",
+ "H4": "GPO65/CR_SOUT1/FLPRG1#",
+ "H5": "GPIO74",
+ "H6": "PSL_GPO/GPOD7",
+ "H7": "GPIOC5/KBRST#",
+ "H8": "GPIOC0/PWM6",
+ "H9": "GPIOC1/I2C6_SDA0",
+ "J1": "eSPI_IO1/GPIO47/F_DIO1/SHI_SDO",
+ "J10": "GPIOB3/I2C7_SCL0/DCD#",
+ "J11": "GPIOA7/PS2_DAT3/TB2/F_DIO3",
+ "J12": "VSS_6",
+ "J2": "GPIO63/PS2_DAT1",
+ "J3": "GPIO67/PS2_CLK0",
+ "J4": "GPIO70/PS2_DAT0",
+ "J5": "GPIO76/EC_SCI#",
+ "J6": "GPIO75/32KHZ_OUT/RXD/CR_SIN2",
+ "J7": "GPIOB7/PWM5",
+ "J8": "PSL_OUT/GPO85",
+ "J9": "GPO86/TXD/CR_SOUT2/FLPRG2#",
+ "K1": "eSPI_IO2/GPIO51/F_DIO2",
+ "K10": "GPIOB2/I2C7_SDA0/DSR#",
+ "K11": "GPIOA5",
+ "K12": "SPIP_MOSI/GPIOA3",
+ "K2": "GPO35/CR_SOUT4/TEST#",
+ "K3": "eSPI_RST#/GPIO54",
+ "K4": "GPIO61/PWROFF#",
+ "K5": "GPIO80/PWM3",
+ "K6": "VCC1_RST#/GPO77",
+ "K7": "GPIO87/I2C1_SDA0",
+ "K8": "GPIO90/I2C1_SCL0",
+ "K9": "GPIO91/I2C2_SDA0",
+ "L1": "eSPI_IO3/GPIO52/F_DIO3",
+ "L10": "GPIO97",
+ "L11": "GPIOB0",
+ "L12": "SPIP_SCLK/GPIOA1",
+ "L2": "eSPI_CS#/GPIO53/SHI_CS#",
+ "L3": "eSPI_ALERT#/GPIO57",
+ "L4": "VBAT",
+ "L5": "32KXIN",
+ "L6": "GPIOE4/I2C6_SCL1/I3C_SCL",
+ "L7": "GPIOE3/I2C6_SDA1/I3C_SDA",
+ "L8": "GPIO92/I2C2_SCL0",
+ "L9": "GPIOB6/PWM4",
+ "M1": "eSPI_CLK/GPIO55/F_SCLK/SHI_SCLK",
+ "M10": "VSBY",
+ "M11": "GPIO94",
+ "M12": "SPIP_MISO/GPIO95",
+ "M2": "GPIO56",
+ "M3": "VSS_7",
+ "M4": "PWRGD/GPIO72",
+ "M5": "32KXOUT",
+ "M6": "VREF_PECI",
+ "M7": "PECI_DATA/GPIO81",
+ "M8": "VCC1_4",
+ "M9": "VSS_8"
+ }
+}
diff --git a/ec_fw/generator.go b/ec_fw/generator.go
new file mode 100644
index 0000000..f7dd536
--- /dev/null
+++ b/ec_fw/generator.go
@@ -0,0 +1,717 @@
+// Copyright 2026 The ChromiumOS Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+import (
+ "cmp"
+ "fmt"
+ "io"
+ "maps"
+ "regexp"
+ "slices"
+ "strconv"
+ "strings"
+)
+
+var (
+ digitsRegex = regexp.MustCompile(`[0-9]+`)
+ npcxSclPinRegex = regexp.MustCompile(`I2C([0-9]+)_SCL([0-9]+)`)
+ npcxI2cIndexRegex = regexp.MustCompile(`I2C[0-9]+_(SCL|SDA)([0-9]+)`)
+ netNameReplacer = strings.NewReplacer(" ", "", "/", "_", "#", "_L")
+)
+
+type gpioPin struct {
+ label string
+ pin string
+ port string
+ pinNum int
+ flags string
+ enum string
+}
+
+type adcPin struct {
+ label string
+ pin string
+ channel int
+ enum string
+}
+
+type i2cPort struct {
+ label string
+ pin string
+ channel string
+ portNode string
+ enums []string
+}
+
+// dtsiGenerator holds the state for generating the DTSI file.
+type dtsiGenerator struct {
+ w io.Writer
+ chip *Chip
+ gpios []gpioPin
+ adcs []adcPin
+ i2cPorts []i2cPort
+ i2cEnables map[string][]string
+ pwmEnables map[int]string
+ adcEnabled bool
+ adcPinctrls []string
+ tachEnables map[int]string
+ tachPorts map[int]string
+ kbPins []string
+ maxKsi int
+ maxKso int
+}
+
+func newDTSIGenerator(w io.Writer, chip *Chip) *dtsiGenerator {
+ return &dtsiGenerator{
+ w: w,
+ chip: chip,
+ i2cEnables: make(map[string][]string),
+ pwmEnables: make(map[int]string),
+ tachEnables: make(map[int]string),
+ tachPorts: make(map[int]string),
+ maxKsi: -1,
+ maxKso: -1,
+ }
+}
+
+// GenerateDTSI generates the Zephyr DTSI content and writes it to w.
+func GenerateDTSI(w io.Writer, configs []PinConfig, chip *Chip) error {
+ g := newDTSIGenerator(w, chip)
+ processedPins := make(map[string]bool)
+
+ for _, cfg := range configs {
+ if processedPins[cfg.PinNumber] {
+ continue
+ }
+ processedPins[cfg.PinNumber] = true
+
+ if cfg.NetName == "" {
+ continue
+ }
+
+ netName := cfg.NetName
+ if cfg.NetNameOverride != "" {
+ netName = cfg.NetNameOverride
+ }
+ netName = cleanNetName(netName)
+
+ // Check excluded net names
+ if strings.HasPrefix(netName, "ESPI_") || strings.HasPrefix(netName, "UART_") || netName == "EC_RST_ODL" {
+ continue
+ }
+
+ if cfg.PinTypeOverride == "OTHER" {
+ continue
+ }
+
+ port, pinNum, gpioErr := chip.ResolveGPIO(cfg.PinNumber)
+ gpName := ""
+ if gpioErr == nil {
+ gpName = fmt.Sprintf("gp%s%d", port, pinNum)
+ }
+
+ // Try processing as Keyboard matrix
+ if g.tryProcessKeyboard(cfg, netName, gpName) {
+ continue
+ }
+
+ // Try processing as Tachometer
+ processedTach, err := g.tryProcessTach(cfg, netName, gpName)
+ if err != nil {
+ return err
+ }
+ if processedTach {
+ continue
+ }
+
+ // Try processing as ADC
+ processedADC, err := g.tryProcessADC(cfg, netName, gpName)
+ if err != nil {
+ return err
+ }
+ if processedADC {
+ continue
+ }
+
+ // Try processing as PWM
+ processedPWM, err := g.tryProcessPWM(cfg, port, pinNum, gpName)
+ if err != nil {
+ return err
+ }
+ if processedPWM {
+ continue
+ }
+
+ // Try processing as I2C
+ processedI2C, err := g.tryProcessI2C(cfg, netName, port, pinNum, gpName)
+ if err != nil {
+ return err
+ }
+ if processedI2C {
+ continue
+ }
+
+ // Process as regular GPIO
+ if isGPIO(cfg) {
+ if gpioErr != nil {
+ return fmt.Errorf("failed to resolve GPIO for pin %s: %v", cfg.PinNumber, gpioErr)
+ }
+ g.processGPIO(cfg, netName, port, pinNum, gpName)
+ }
+ }
+
+ return g.write()
+}
+
+func (g *dtsiGenerator) tryProcessKeyboard(cfg PinConfig, netName, gpName string) bool {
+ isKB := cfg.PinTypeOverride == "KEYBOARD"
+ if !isKB {
+ return false
+ }
+
+ prefix := "kso"
+ if strings.Contains(netName, "KSI") {
+ prefix = "ksi"
+ }
+ idx := findDigits(netName)
+ if idx == "" {
+ return false
+ }
+
+ idxNum, err := strconv.Atoi(idx)
+ if err != nil {
+ return false
+ }
+
+ if prefix == "ksi" {
+ if idxNum > g.maxKsi {
+ g.maxKsi = idxNum
+ }
+ } else {
+ if idxNum > g.maxKso {
+ g.maxKso = idxNum
+ }
+ }
+
+ if len(idx) > 1 && idx[0] == '0' {
+ if prefix == "ksi" || !g.chip.IsNPCX() {
+ idx = idx[1:]
+ }
+ }
+
+ if g.chip.IsNPCX() {
+ if gpName != "" {
+ g.kbPins = append(g.kbPins, fmt.Sprintf("&%s%s_%s", prefix, idx, gpName))
+ }
+ } else {
+ g.kbPins = append(g.kbPins, fmt.Sprintf("&%s%s_default", prefix, idx))
+ }
+ return true
+}
+
+func (g *dtsiGenerator) tryProcessTach(cfg PinConfig, netName, gpName string) (bool, error) {
+ isTach := cfg.PinTypeOverride == "TACH" || strings.HasSuffix(netName, "_TACH")
+ if !isTach {
+ return false, nil
+ }
+
+ chanNum, portLetter, err := g.chip.ResolveTACH(cfg.PinNumber)
+ if err != nil {
+ return false, fmt.Errorf("failed to resolve TACH for pin %s: %w", cfg.PinNumber, err)
+ }
+
+ if g.chip.IsNPCX() {
+ pinctrl := fmt.Sprintf("&t%s%d_1_in_%s", strings.ToLower(portLetter), chanNum, gpName)
+ g.tachEnables[chanNum] = pinctrl
+ g.tachPorts[chanNum] = portLetter
+ } else {
+ // ITE
+ pinctrl := fmt.Sprintf("&tach%d%s_%s_default", chanNum, strings.ToLower(portLetter), gpName)
+ g.tachEnables[chanNum] = pinctrl
+ g.tachPorts[chanNum] = portLetter
+ }
+ return true, nil
+}
+
+func (g *dtsiGenerator) tryProcessADC(cfg PinConfig, netName, gpName string) (bool, error) {
+ isADC := cfg.IO == "ADC" || cfg.PinTypeOverride == "ADC"
+ if !isADC {
+ return false, nil
+ }
+
+ adcChan, err := g.chip.ResolveADC(cfg.PinNumber)
+ if err != nil {
+ return false, fmt.Errorf("failed to resolve ADC for pin %s: %v", cfg.PinNumber, err)
+ }
+ g.adcEnabled = true
+ label := fmt.Sprintf("adc_%s", strings.ToLower(netName))
+ g.adcs = append(g.adcs, adcPin{
+ label: label,
+ pin: strings.ToLower(netName),
+ channel: adcChan,
+ enum: cfg.Enum,
+ })
+
+ if gpName != "" {
+ if g.chip.IsNPCX() {
+ g.adcPinctrls = append(g.adcPinctrls, fmt.Sprintf("&adc0_chan%d_%s", adcChan, gpName))
+ } else {
+ g.adcPinctrls = append(g.adcPinctrls, fmt.Sprintf("&adc0_ch%d_%s_default", adcChan, gpName))
+ }
+ }
+ return true, nil
+}
+
+func (g *dtsiGenerator) tryProcessPWM(cfg PinConfig, port string, pinNum int, gpName string) (bool, error) {
+ isPWM := cfg.IO == "PWM" || cfg.PinTypeOverride == "PWM" || cfg.PinTypeOverride == "PWM_INVERT"
+ if !isPWM {
+ return false, nil
+ }
+
+ pwmChan, err := g.chip.ResolvePWM(cfg.PinNumber)
+ if err != nil {
+ return false, fmt.Errorf("failed to resolve PWM for pin %s: %v", cfg.PinNumber, err)
+ }
+ if gpName != "" {
+ pinctrl := fmt.Sprintf("&pwm%d_gp%s%d", pwmChan, port, pinNum)
+ if !g.chip.IsNPCX() {
+ pinctrl = fmt.Sprintf("&pwm%d_%s_default", pwmChan, gpName)
+ }
+ g.pwmEnables[pwmChan] = pinctrl
+ }
+ return true, nil
+}
+
+func (g *dtsiGenerator) tryProcessI2C(cfg PinConfig, netName string, port string, pinNum int, gpName string) (bool, error) {
+ isI2C := cfg.IO == "OD" && (cfg.PinTypeOverride == "I2C_CLOCK" || cfg.PinTypeOverride == "I2C_DATA" || cfg.PinTypeOverride == "I2C")
+ if !isI2C {
+ return false, nil
+ }
+
+ i2cChan, isScl, err := g.chip.ResolveI2C(cfg.PinNumber)
+ if err != nil {
+ return false, fmt.Errorf("failed to resolve I2C for pin %s: %v", cfg.PinNumber, err)
+ }
+
+ if !isScl {
+ return true, nil
+ }
+
+ baseName := strings.ToLower(netName)
+ baseName = strings.TrimSuffix(baseName, "_scl")
+
+ label := fmt.Sprintf("i2c_%s", baseName)
+ portName := baseName
+
+ var enums []string
+ if cfg.Enum != "" {
+ enums = append(enums, cfg.Enum)
+ }
+
+ portNode := fmt.Sprintf("i2c%s", i2cChan)
+ if g.chip.IsNPCX() {
+ pinDef := g.chip.PinMap[cfg.PinNumber]
+ idx := extractNPCXI2CIndex(pinDef)
+ portNode = fmt.Sprintf("i2c%s_%d", i2cChan, idx)
+ }
+
+ g.i2cPorts = append(g.i2cPorts, i2cPort{
+ label: label,
+ pin: portName,
+ channel: i2cChan,
+ portNode: portNode,
+ enums: enums,
+ })
+
+ if g.chip.IsNPCX() {
+ sdaGP, err := findNPCXSDAGPIO(g.chip, cfg.PinNumber)
+ if err != nil {
+ return false, fmt.Errorf("failed to find NPCX SDA GPIO for pin %s: %v", cfg.PinNumber, err)
+ }
+ pinDef := g.chip.PinMap[cfg.PinNumber]
+ idx := extractNPCXI2CIndex(pinDef)
+ sclGP := fmt.Sprintf("gp%s%d", port, pinNum)
+ gpios := []string{sdaGP, sclGP}
+ slices.Sort(gpios)
+ first := gpios[0]
+ second := strings.TrimPrefix(gpios[1], "gp")
+ pinctrl := fmt.Sprintf("&i2c%s_%d_sda_scl_%s_%s", i2cChan, idx, first, second)
+ nodeName := fmt.Sprintf("i2c%s_%d", i2cChan, idx)
+ g.i2cEnables[nodeName] = []string{pinctrl}
+ } else {
+ // ITE
+ sdaGP, err := findITESDAGPIO(g.chip, cfg.PinNumber)
+ if err != nil {
+ return false, fmt.Errorf("failed to find ITE SDA GPIO for pin %s: %v", cfg.PinNumber, err)
+ }
+ i2cNodeTrim := "i2c" + strings.TrimSuffix(i2cChan, "ALT")
+ clkPinctrl := fmt.Sprintf("&%s_clk_%s_default", i2cNodeTrim, gpName)
+ sdaPinctrl := fmt.Sprintf("&%s_data_%s_default", i2cNodeTrim, sdaGP)
+ nodeName := "i2c" + i2cChan
+ g.i2cEnables[nodeName] = append(g.i2cEnables[nodeName], clkPinctrl, sdaPinctrl)
+ }
+ return true, nil
+}
+
+func (g *dtsiGenerator) processGPIO(cfg PinConfig, netName, port string, pinNum int, gpName string) {
+ if gpName != "" {
+ label := fmt.Sprintf("gpio_%s", strings.ToLower(netName))
+ flags := determineGPIOFlags(cfg)
+ g.gpios = append(g.gpios, gpioPin{
+ label: label,
+ pin: strings.ToLower(netName),
+ port: port,
+ pinNum: pinNum,
+ flags: flags,
+ enum: cfg.Enum,
+ })
+ }
+}
+
+func (g *dtsiGenerator) write() error {
+ ew := &errWriter{w: g.w}
+ g.w = ew
+
+ // Sort lists deterministically
+ slices.SortStableFunc(g.gpios, func(a, b gpioPin) int { return cmp.Compare(a.pin, b.pin) })
+ slices.SortStableFunc(g.adcs, func(a, b adcPin) int { return cmp.Compare(a.pin, b.pin) })
+ slices.SortStableFunc(g.i2cPorts, func(a, b i2cPort) int { return cmp.Compare(a.pin, b.pin) })
+ slices.Sort(g.adcPinctrls)
+
+ // Write header
+ fmt.Fprintln(g.w, "/* Copyright 2026 The ChromiumOS Authors")
+ fmt.Fprintln(g.w, " * Use of this source code is governed by a BSD-style license that can be")
+ fmt.Fprintln(g.w, " * found in the LICENSE file.")
+ fmt.Fprintln(g.w, " *")
+ fmt.Fprintln(g.w, " * This file is auto-generated - do not edit!")
+ fmt.Fprintln(g.w, " */")
+ fmt.Fprintln(g.w)
+ fmt.Fprintln(g.w, "/ {")
+ fmt.Fprintln(g.w)
+
+ // Write ADC channels
+ if len(g.adcs) > 0 {
+ fmt.Fprintln(g.w, "\tnamed-adc-channels {")
+ fmt.Fprintln(g.w, "\t\tcompatible = \"named-adc-channels\";")
+ fmt.Fprintln(g.w)
+ for _, adc := range g.adcs {
+ fmt.Fprintf(g.w, "\t\t%s: %s {\n", adc.label, adc.pin)
+ if adc.enum != "" {
+ fmt.Fprintf(g.w, "\t\t\tenum-name = \"%s\";\n", adc.enum)
+ }
+ fmt.Fprintf(g.w, "\t\t\tio-channels = <&adc0 %d>;\n", adc.channel)
+ fmt.Fprintln(g.w, "\t\t};")
+ }
+ fmt.Fprintln(g.w, "\t};")
+ fmt.Fprintln(g.w)
+ }
+
+ // Write Named GPIOs
+ if len(g.gpios) > 0 {
+ fmt.Fprintln(g.w, "\tnamed-gpios {")
+ fmt.Fprintln(g.w, "\t\tcompatible = \"named-gpios\";")
+ fmt.Fprintln(g.w)
+ for _, gp := range g.gpios {
+ fmt.Fprintf(g.w, "\t\t%s: %s {\n", gp.label, gp.pin)
+ fmt.Fprintf(g.w, "\t\t\tgpios = <&gpio%s %d %s>;\n", gp.port, gp.pinNum, gp.flags)
+ if gp.enum != "" {
+ fmt.Fprintf(g.w, "\t\t\tenum-name = \"%s\";\n", gp.enum)
+ }
+ fmt.Fprintln(g.w, "\t\t};")
+ }
+ fmt.Fprintln(g.w, "\t};")
+ fmt.Fprintln(g.w)
+ }
+
+ // Write Named I2C ports
+ if len(g.i2cPorts) > 0 {
+ fmt.Fprintln(g.w, "\tnamed-i2c-ports {")
+ fmt.Fprintln(g.w, "\t\tcompatible = \"named-i2c-ports\";")
+ fmt.Fprintln(g.w)
+ for _, i2c := range g.i2cPorts {
+ fmt.Fprintf(g.w, "\t\t%s: %s {\n", i2c.label, i2c.pin)
+ fmt.Fprintf(g.w, "\t\t\ti2c-port = <&%s>;\n", strings.TrimSuffix(i2c.portNode, "ALT"))
+ if len(i2c.enums) > 0 {
+ fmt.Fprintf(g.w, "\t\t\tenum-names = \"%s\";\n", strings.Join(i2c.enums, "\", \""))
+ }
+ fmt.Fprintln(g.w, "\t\t};")
+ }
+ fmt.Fprintln(g.w, "\t};")
+ fmt.Fprintln(g.w)
+ }
+
+ fmt.Fprintln(g.w, "};")
+ fmt.Fprintln(g.w)
+
+ // Write ADC pinctrl
+ if len(g.adcPinctrls) > 0 {
+ fmt.Fprintln(g.w, "&adc0 {")
+ fmt.Fprintln(g.w, "\tstatus = \"okay\";")
+ if len(g.adcPinctrls) == 1 {
+ fmt.Fprintf(g.w, "\tpinctrl-0 = <%s>;\n", g.adcPinctrls[0])
+ } else {
+ fmt.Fprintln(g.w, "\tpinctrl-0 = <")
+ for _, p := range g.adcPinctrls {
+ fmt.Fprintf(g.w, "\t\t%s\n", p)
+ }
+ fmt.Fprintln(g.w, "\t>;")
+ }
+ fmt.Fprintln(g.w, "\tpinctrl-names = \"default\";")
+ fmt.Fprintln(g.w, "};")
+ fmt.Fprintln(g.w)
+ }
+
+ // Write I2C pinctrl
+ i2cNodes := slices.Collect(maps.Keys(g.i2cEnables))
+ slices.Sort(i2cNodes)
+
+ for _, nodeName := range i2cNodes {
+ pinctrls := g.i2cEnables[nodeName]
+ fmt.Fprintf(g.w, "&%s {\n", nodeName)
+ fmt.Fprintln(g.w, "\tstatus = \"okay\";")
+ if len(pinctrls) == 1 {
+ fmt.Fprintf(g.w, "\tpinctrl-0 = <%s>;\n", pinctrls[0])
+ } else {
+ fmt.Fprintln(g.w, "\tpinctrl-0 = <")
+ for _, p := range pinctrls {
+ fmt.Fprintf(g.w, "\t\t%s\n", p)
+ }
+ fmt.Fprintln(g.w, "\t>;")
+ }
+ fmt.Fprintln(g.w, "\tpinctrl-names = \"default\";")
+ fmt.Fprintln(g.w, "};")
+ fmt.Fprintln(g.w)
+ }
+
+ // Write PWM pinctrl
+ pwmChans := slices.Collect(maps.Keys(g.pwmEnables))
+ slices.Sort(pwmChans)
+
+ for _, ch := range pwmChans {
+ fmt.Fprintf(g.w, "&pwm%d {\n", ch)
+ fmt.Fprintln(g.w, "\tstatus = \"okay\";")
+ fmt.Fprintf(g.w, "\tpinctrl-0 = <%s>;\n", g.pwmEnables[ch])
+ fmt.Fprintln(g.w, "\tpinctrl-names = \"default\";")
+ fmt.Fprintln(g.w, "};")
+ fmt.Fprintln(g.w)
+ }
+
+ // Write Tach pinctrl
+ tachChans := slices.Collect(maps.Keys(g.tachEnables))
+ slices.Sort(tachChans)
+
+ for _, ch := range tachChans {
+ if g.chip.IsNPCX() {
+ portLetter := g.tachPorts[ch]
+ fmt.Fprintf(g.w, "&tach%d {\n", ch)
+ fmt.Fprintln(g.w, "\tstatus = \"okay\";")
+ fmt.Fprintf(g.w, "\tpinctrl-0 = <%s>;\n", g.tachEnables[ch])
+ fmt.Fprintln(g.w, "\tpinctrl-names = \"default\";")
+ fmt.Fprintf(g.w, "\tport = <&gpio%s>;\n", strings.ToLower(portLetter))
+ fmt.Fprintln(g.w, "};")
+ fmt.Fprintln(g.w)
+ } else {
+ fmt.Fprintf(g.w, "&tach%d {\n", ch)
+ fmt.Fprintln(g.w, "\tstatus = \"okay\";")
+ fmt.Fprintf(g.w, "\tpinctrl-0 = <%s>;\n", g.tachEnables[ch])
+ fmt.Fprintln(g.w, "\tpinctrl-names = \"default\";")
+ fmt.Fprintln(g.w, "};")
+ fmt.Fprintln(g.w)
+ }
+ }
+
+ // Write Keyboard matrix pinctrl
+ if len(g.kbPins) > 0 {
+ slices.Sort(g.kbPins)
+ fmt.Fprintln(g.w, "&kbd {")
+ fmt.Fprintln(g.w, "\tstatus = \"okay\";")
+ fmt.Fprintln(g.w, "\tpinctrl-0 = <")
+ for _, p := range g.kbPins {
+ fmt.Fprintf(g.w, "\t\t%s\n", p)
+ }
+ fmt.Fprintln(g.w, "\t>;")
+ fmt.Fprintln(g.w, "\tpinctrl-names = \"default\";")
+ rowSize := 0
+ if g.maxKsi >= 0 {
+ rowSize = g.maxKsi + 1
+ }
+ colSize := 0
+ if g.maxKso >= 0 {
+ colSize = g.maxKso + 1
+ }
+ fmt.Fprintf(g.w, "\trow-size = <%d>;\n", rowSize)
+ fmt.Fprintf(g.w, "\tcol-size = <%d>;\n", colSize)
+ fmt.Fprintln(g.w, "};")
+ }
+
+ return ew.err
+}
+
+func findSDAGPIO(chip *Chip, sdaFunc string) (string, error) {
+ var pins []string
+ if len(chip.PkgPins) > 0 {
+ pins = slices.Clone(chip.PkgPins)
+ } else {
+ pins = slices.Collect(maps.Keys(chip.PinMap))
+ slices.Sort(pins)
+ }
+
+ for _, pinNum := range pins {
+ def := chip.PinMap[pinNum]
+ if hasFunction(def, sdaFunc) {
+ port, pin, err := chip.ResolveGPIO(pinNum)
+ if err != nil {
+ return "", err
+ }
+ return fmt.Sprintf("gp%s%d", port, pin), nil
+ }
+ }
+ return "", fmt.Errorf("matching SDA pin function %s not found in chip map", sdaFunc)
+}
+
+func findNPCXSDAGPIO(chip *Chip, sclPinNum string) (string, error) {
+ sclDef := chip.PinMap[sclPinNum]
+ m := npcxSclPinRegex.FindStringSubmatch(sclDef)
+ if m == nil {
+ return "", fmt.Errorf("not an NPCX SCL pin definition: %s", sclDef)
+ }
+ sdaFunc := fmt.Sprintf("I2C%s_SDA%s", m[1], m[2])
+ return findSDAGPIO(chip, sdaFunc)
+}
+
+func findITESDAGPIO(chip *Chip, sclPinNum string) (string, error) {
+ sclDef := chip.PinMap[sclPinNum]
+ m := iteScl.FindStringSubmatch(sclDef)
+ if m == nil {
+ return "", fmt.Errorf("not an ITE SCL pin definition: %s", sclDef)
+ }
+ sdaFunc := fmt.Sprintf("SMDAT%s", m[1])
+ return findSDAGPIO(chip, sdaFunc)
+}
+
+func extractNPCXI2CIndex(pinDef string) int {
+ m := npcxI2cIndexRegex.FindStringSubmatch(pinDef)
+ if m != nil {
+ idx, err := strconv.Atoi(m[2])
+ if err == nil {
+ return idx
+ }
+ }
+ return 0
+}
+
+func cleanNetName(name string) string {
+ return netNameReplacer.Replace(name)
+}
+
+func isGPIO(cfg PinConfig) bool {
+ if cfg.Dir == "IN" || cfg.Dir == "OUT" {
+ return true
+ }
+ switch cfg.PinTypeOverride {
+ case "INPUT", "INPUT_L", "OUTPUT", "OUTPUT_L", "OUTPUT_ACTIVE_L", "OUTPUT_ACTIVE_H", "ODR_ACTIVE_L", "ODR_ACTIVE_H", "OUTPUT_ODL", "OUTPUT_ODR":
+ return true
+ }
+ return false
+}
+
+func hasFunction(pinDef, function string) bool {
+ parts := strings.Split(pinDef, "/")
+ for _, part := range parts {
+ if strings.TrimSpace(part) == function {
+ return true
+ }
+ }
+ return false
+}
+
+func findDigits(s string) string {
+ return digitsRegex.FindString(s)
+}
+
+func determineGPIOFlags(cfg PinConfig) string {
+ var flags []string
+
+ isOutput := cfg.Dir == "OUT" ||
+ cfg.PinTypeOverride == "OUTPUT" ||
+ cfg.PinTypeOverride == "OUTPUT_L" ||
+ cfg.PinTypeOverride == "OUTPUT_H" ||
+ cfg.PinTypeOverride == "OUTPUT_ACTIVE_L" ||
+ cfg.PinTypeOverride == "OUTPUT_ACTIVE_H" ||
+ cfg.PinTypeOverride == "OUTPUT_ODL" ||
+ cfg.PinTypeOverride == "OUTPUT_ODR" ||
+ cfg.PinTypeOverride == "ODR_ACTIVE_L" ||
+ cfg.PinTypeOverride == "ODR_ACTIVE_H"
+
+ if isOutput {
+ flags = append(flags, "GPIO_OUTPUT")
+ } else {
+ flags = append(flags, "GPIO_INPUT")
+ }
+
+ isOD := cfg.IO == "OD" ||
+ cfg.PinTypeOverride == "OUTPUT_ODL" ||
+ cfg.PinTypeOverride == "OUTPUT_ODR" ||
+ cfg.PinTypeOverride == "ODR_ACTIVE_L" ||
+ cfg.PinTypeOverride == "ODR_ACTIVE_H"
+
+ if isOD && (isOutput || cfg.Dir == "IO") {
+ flags = append(flags, "GPIO_OPEN_DRAIN")
+ }
+
+ isActiveLow := cfg.PinTypeOverride == "INPUT_L" ||
+ cfg.PinTypeOverride == "OUTPUT_L" ||
+ cfg.PinTypeOverride == "OUTPUT_ACTIVE_L" ||
+ cfg.PinTypeOverride == "OUTPUT_ODL" ||
+ cfg.PinTypeOverride == "ODR_ACTIVE_L"
+
+ if !isActiveLow && (cfg.PinTypeOverride == "" || cfg.PinTypeOverride == "INPUT" || cfg.PinTypeOverride == "OUTPUT") {
+ cleanName := strings.ToLower(cfg.NetName)
+ if strings.HasSuffix(cleanName, "_l") || strings.HasSuffix(cleanName, "_odl") || strings.HasSuffix(cleanName, "_low") {
+ isActiveLow = true
+ }
+ }
+
+ if isActiveLow {
+ flags = append(flags, "GPIO_ACTIVE_LOW")
+ } else {
+ flags = append(flags, "GPIO_ACTIVE_HIGH")
+ }
+
+ if cfg.Pull == "UP" {
+ flags = append(flags, "GPIO_PULL_UP")
+ } else if cfg.Pull == "DOWN" {
+ flags = append(flags, "GPIO_PULL_DOWN")
+ }
+
+ if isOutput {
+ if cfg.InitialCondition == "HIGH" {
+ flags = append(flags, "GPIO_OUTPUT_INIT_HIGH")
+ } else if cfg.InitialCondition == "LOW" {
+ flags = append(flags, "GPIO_OUTPUT_INIT_LOW")
+ }
+ }
+
+ if cfg.Voltage == "1.8V" {
+ flags = append(flags, "GPIO_VOLTAGE_1P8")
+ }
+
+ return "(" + strings.Join(flags, " | ") + ")"
+}
+
+type errWriter struct {
+ w io.Writer
+ err error
+}
+
+func (ew *errWriter) Write(p []byte) (int, error) {
+ if ew.err != nil {
+ return 0, ew.err
+ }
+ var n int
+ n, ew.err = ew.w.Write(p)
+ return n, ew.err
+}
diff --git a/ec_fw/generator_test.go b/ec_fw/generator_test.go
new file mode 100644
index 0000000..629b109
--- /dev/null
+++ b/ec_fw/generator_test.go
@@ -0,0 +1,421 @@
+// Copyright 2026 The ChromiumOS Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+import (
+ "bytes"
+ "os"
+ "strings"
+ "testing"
+)
+
+func TestGenerateDTSI_Mock(t *testing.T) {
+ mockPinMap := map[string]string{
+ "A1": "ADC0",
+ "B2": "GPIOB4/I2C0_SDA0",
+ "B3": "GPIOB5/I2C0_SCL0",
+ "C3": "GPIOC3/PWM0",
+ "D4": "GPIOD4",
+ "E5": "GPIOE5",
+ "F5": "GPIOF5/TB1",
+ "G1": "KSI0/GPIO31",
+ "G2": "KSO01/GPIO21",
+ }
+
+ chip := &Chip{
+ Name: "npcx-mock",
+ Vendor: "npcx",
+ PinMap: mockPinMap,
+ }
+
+ configs := []PinConfig{
+ {
+ PinNumber: "A1",
+ PinName: "ADC_0",
+ NetName: "EC_ADC0",
+ IO: "ADC",
+ Enum: "ADC_EVB_CH_0",
+ },
+ {
+ PinNumber: "B3",
+ PinName: "I2C0_SCL",
+ NetName: "EC_I2C_SCL",
+ Enum: "I2C_PORT_0",
+ IO: "OD",
+ PinTypeOverride: "I2C_CLOCK",
+ },
+ {
+ PinNumber: "B2",
+ PinName: "I2C0_SDA",
+ NetName: "EC_I2C_SDA",
+ IO: "OD",
+ PinTypeOverride: "I2C_DATA",
+ },
+ {
+ PinNumber: "C3",
+ PinName: "PWM_0",
+ NetName: "EC_PWM0",
+ IO: "PWM",
+ },
+ {
+ PinNumber: "D4",
+ PinName: "GPIO_D4",
+ NetName: "EC_GPIO_D4",
+ Dir: "OUT",
+ IO: "OD",
+ },
+ {
+ PinNumber: "E5",
+ PinName: "GPIO_E5",
+ NetName: "EC_GPIO_E5_L",
+ Dir: "IN",
+ Pull: "UP",
+ PinTypeOverride: "INPUT_L",
+ },
+ {
+ PinNumber: "F5",
+ PinName: "TACH_1",
+ NetName: "EC_FAN_TACH",
+ },
+ {
+ PinNumber: "G1",
+ PinName: "KSI0",
+ NetName: "EC_KSI0",
+ PinTypeOverride: "KEYBOARD",
+ },
+ {
+ PinNumber: "G2",
+ PinName: "KSO01",
+ NetName: "EC_KSO01",
+ PinTypeOverride: "KEYBOARD",
+ },
+ }
+
+ var out bytes.Buffer
+ err := GenerateDTSI(&out, configs, chip)
+ if err != nil {
+ t.Fatalf("GenerateDTSI failed: %v", err)
+ }
+
+ got := out.String()
+ requiredSnippets := []string{
+ "named-adc-channels",
+ "named-gpios",
+ "named-i2c-ports",
+ "&adc0",
+ "&pwm0",
+ "&tach1",
+ "&kbd",
+ }
+
+ for _, snip := range requiredSnippets {
+ if !strings.Contains(got, snip) {
+ t.Errorf("GenerateDTSI output missing required snippet %q", snip)
+ }
+ }
+}
+
+func TestGenerateDTSI_ITE_Mock(t *testing.T) {
+ mockPinMap := map[string]string{
+ "A1": "ADC0/GPA0",
+ "B3": "SMCLK0/GPB3",
+ "B4": "SMDAT0/GPB4",
+ "C3": "PWM0/GPC3",
+ "D4": "GPIOD4",
+ "D6": "TACH0A/GPD6",
+ "G1": "KSI0/GPG1",
+ "G2": "KSO0/GPG2",
+ }
+
+ chip := &Chip{
+ Name: "ite-mock",
+ Vendor: "ite",
+ PinMap: mockPinMap,
+ }
+
+ configs := []PinConfig{
+ {
+ PinNumber: "A1",
+ PinName: "ADC_0",
+ NetName: "EC_ADC0",
+ IO: "ADC",
+ Enum: "ADC_EVB_CH_0",
+ },
+ {
+ PinNumber: "B3",
+ PinName: "I2C0_SCL",
+ NetName: "EC_I2C",
+ Enum: "I2C_PORT_0",
+ IO: "OD",
+ PinTypeOverride: "I2C_CLOCK",
+ },
+ {
+ PinNumber: "B4",
+ PinName: "I2C0_SDA",
+ NetName: "EC_I2C",
+ IO: "OD",
+ PinTypeOverride: "I2C_DATA",
+ },
+ {
+ PinNumber: "C3",
+ PinName: "PWM_0",
+ NetName: "EC_PWM0",
+ IO: "PWM",
+ },
+ {
+ PinNumber: "D6",
+ PinName: "TACH_0",
+ NetName: "EC_FAN_TACH",
+ PinTypeOverride: "TACH",
+ },
+ {
+ PinNumber: "G1",
+ PinName: "KSI0",
+ NetName: "EC_KSI0",
+ PinTypeOverride: "KEYBOARD",
+ },
+ {
+ PinNumber: "G2",
+ PinName: "KSO0",
+ NetName: "EC_KSO0",
+ PinTypeOverride: "KEYBOARD",
+ },
+ }
+
+ var out bytes.Buffer
+ err := GenerateDTSI(&out, configs, chip)
+ if err != nil {
+ t.Fatalf("GenerateDTSI failed: %v", err)
+ }
+
+ got := out.String()
+ requiredSnippets := []string{
+ "named-adc-channels",
+ "named-i2c-ports",
+ "&adc0",
+ "&i2c0",
+ "&pwm0",
+ "&tach0",
+ "&kbd",
+ }
+
+ for _, snip := range requiredSnippets {
+ if !strings.Contains(got, snip) {
+ t.Errorf("ITE DTSI output missing snippet %q", snip)
+ }
+ }
+}
+
+func TestGenerateDTSI_SubsystemErrorPaths(t *testing.T) {
+ chip := &Chip{
+ Name: "npcx-mock",
+ Vendor: "npcx",
+ PinMap: map[string]string{"D4": "GPIOD4"}, // D4 supports no peripheral
+ }
+
+ // Test ADC failure on non-ADC pin
+ adcConfig := []PinConfig{{PinNumber: "D4", NetName: "ADC_FAIL", IO: "ADC"}}
+ var buf bytes.Buffer
+ if err := GenerateDTSI(&buf, adcConfig, chip); err == nil || !strings.Contains(err.Error(), "failed to resolve ADC") {
+ t.Errorf("Expected ADC resolution error, got %v", err)
+ }
+
+ // Test PWM failure on non-PWM pin
+ pwmConfig := []PinConfig{{PinNumber: "D4", NetName: "PWM_FAIL", IO: "PWM"}}
+ buf.Reset()
+ if err := GenerateDTSI(&buf, pwmConfig, chip); err == nil || !strings.Contains(err.Error(), "failed to resolve PWM") {
+ t.Errorf("Expected PWM resolution error, got %v", err)
+ }
+
+ // Test I2C failure on non-I2C pin
+ i2cConfig := []PinConfig{{PinNumber: "D4", NetName: "I2C_FAIL", IO: "OD", PinTypeOverride: "I2C_CLOCK"}}
+ buf.Reset()
+ if err := GenerateDTSI(&buf, i2cConfig, chip); err == nil || !strings.Contains(err.Error(), "failed to resolve I2C") {
+ t.Errorf("Expected I2C resolution error, got %v", err)
+ }
+}
+
+func TestGenerateDTSI_DuplicatePinsAndExclusions(t *testing.T) {
+ chip := &Chip{
+ Name: "npcx-mock",
+ Vendor: "npcx",
+ PinMap: map[string]string{
+ "D4": "GPIOD4",
+ "D5": "GPIOD5",
+ "D6": "GPIOD6",
+ },
+ }
+
+ configs := []PinConfig{
+ {PinNumber: "D4", NetName: "EC_GPIO_D4", Dir: "OUT"},
+ {PinNumber: "D4", NetName: "EC_DUPLICATE_D4", Dir: "OUT"}, // Should be ignored
+ {PinNumber: "D5", NetName: "ESPI_CLK", Dir: "IN"}, // Excluded prefix
+ {PinNumber: "D6", NetName: "EC_GPIO_D6", Dir: "OUT", PinTypeOverride: "OTHER"}, // Excluded type
+ }
+
+ var buf bytes.Buffer
+ if err := GenerateDTSI(&buf, configs, chip); err != nil {
+ t.Fatalf("GenerateDTSI failed: %v", err)
+ }
+
+ out := buf.String()
+ if strings.Contains(out, "ec_duplicate_d4") {
+ t.Error("Duplicate pin D4 was not deduplicated")
+ }
+ if strings.Contains(out, "espi_clk") {
+ t.Error("ESPI_CLK signal was not excluded")
+ }
+ if strings.Contains(out, "ec_gpio_d6") {
+ t.Error("PinTypeOverride OTHER was not excluded")
+ }
+ if !strings.Contains(out, "gpio_ec_gpio_d4: ec_gpio_d4") {
+ t.Error("Primary pin D4 missing from output")
+ }
+}
+
+func TestGenerateDTSI_AllGPIOFlags(t *testing.T) {
+ chip := &Chip{
+ Name: "npcx-mock",
+ Vendor: "npcx",
+ PinMap: map[string]string{
+ "A1": "GPIOA1",
+ },
+ }
+
+ configs := []PinConfig{
+ {
+ PinNumber: "A1",
+ NetName: "EC_FLAG_TEST",
+ Dir: "OUT",
+ PinTypeOverride: "ODR_ACTIVE_L",
+ Voltage: "1.8V",
+ InitialCondition: "HIGH",
+ Pull: "DOWN",
+ },
+ }
+
+ var buf bytes.Buffer
+ if err := GenerateDTSI(&buf, configs, chip); err != nil {
+ t.Fatalf("GenerateDTSI failed: %v", err)
+ }
+
+ expectedFlags := "(GPIO_OUTPUT | GPIO_OPEN_DRAIN | GPIO_ACTIVE_LOW | GPIO_PULL_DOWN | GPIO_OUTPUT_INIT_HIGH | GPIO_VOLTAGE_1P8)"
+ if !strings.Contains(buf.String(), expectedFlags) {
+ t.Errorf("Output missing expected flags %q.\nGot:\n%s", expectedFlags, buf.String())
+ }
+}
+
+func TestIntegration_ExampleEcPinout_With_RealChip(t *testing.T) {
+ csvFile := "test/ExampleEcPinout.csv"
+ data, err := os.ReadFile(csvFile)
+ if err != nil {
+ t.Skipf("Skipping integration test: %s not found", csvFile)
+ }
+
+ t.Run("NPCX99", func(t *testing.T) {
+ chip, ok := GetChip("npcx99")
+ if !ok {
+ t.Fatal("npcx99 chip definition missing")
+ }
+
+ configs, err := ParseCSV(bytes.NewReader(data), "NPCX99 (NPCX)")
+ if err != nil {
+ t.Fatalf("ParseCSV failed: %v", err)
+ }
+
+ var buf bytes.Buffer
+ if err := GenerateDTSI(&buf, configs, chip); err != nil {
+ t.Fatalf("GenerateDTSI failed: %v", err)
+ }
+
+ output := buf.String()
+ requiredNodes := []string{
+ "named-adc-channels",
+ "named-gpios",
+ "named-i2c-ports",
+ "&adc0",
+ "&pwm0",
+ }
+
+ for _, req := range requiredNodes {
+ if !strings.Contains(output, req) {
+ t.Errorf("Generated DTSI missing expected node structure %q", req)
+ }
+ }
+ })
+
+ t.Run("IT82002", func(t *testing.T) {
+ chip, ok := GetChip("it82002")
+ if !ok {
+ t.Fatal("it82002 chip definition missing")
+ }
+
+ configs, err := ParseCSV(bytes.NewReader(data), "IT82002 (ITE)")
+ if err != nil {
+ t.Fatalf("ParseCSV failed: %v", err)
+ }
+
+ var buf bytes.Buffer
+ if err := GenerateDTSI(&buf, configs, chip); err != nil {
+ t.Fatalf("GenerateDTSI failed: %v", err)
+ }
+
+ output := buf.String()
+ requiredNodes := []string{
+ "named-adc-channels",
+ "named-gpios",
+ "named-i2c-ports",
+ "&adc0",
+ "&i2c1",
+ "&pwm1",
+ "&pwm6",
+ "&tach0",
+ "&kbd",
+ }
+
+ for _, req := range requiredNodes {
+ if !strings.Contains(output, req) {
+ t.Errorf("Generated DTSI missing expected node structure %q", req)
+ }
+ }
+ })
+}
+
+func TestGenerateDTSI_IsGPIOBranches(t *testing.T) {
+ chip := &Chip{
+ Name: "npcx-mock",
+ Vendor: "npcx",
+ PinMap: map[string]string{
+ "D4": "GPIOD4",
+ "D5": "GPIOD5",
+ },
+ }
+
+ configs := []PinConfig{
+ {
+ PinNumber: "D4",
+ NetName: "EC_GPIO_D4",
+ PinTypeOverride: "INPUT",
+ },
+ {
+ PinNumber: "D5",
+ NetName: "EC_GPIO_D5",
+ PinTypeOverride: "INVALID_GPIO",
+ },
+ }
+
+ var buf bytes.Buffer
+ if err := GenerateDTSI(&buf, configs, chip); err != nil {
+ t.Fatalf("GenerateDTSI failed: %v", err)
+ }
+
+ out := buf.String()
+ if !strings.Contains(out, "gpio_ec_gpio_d4: ec_gpio_d4") {
+ t.Error("Expected D4 to be processed as GPIO because PinTypeOverride is INPUT")
+ }
+ if strings.Contains(out, "gpio_ec_gpio_d5") {
+ t.Error("Expected D5 to NOT be processed as GPIO because PinTypeOverride is INVALID_GPIO")
+ }
+}
diff --git a/ec_fw/main.go b/ec_fw/main.go
new file mode 100644
index 0000000..0846fa8
--- /dev/null
+++ b/ec_fw/main.go
@@ -0,0 +1,90 @@
+// Copyright 2026 The ChromiumOS Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+import (
+ "flag"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+)
+
+func main() {
+ if err := run(); err != nil {
+ fmt.Fprintf(os.Stderr, "%v\n", err)
+ os.Exit(1)
+ }
+}
+
+func run() error {
+ chipFlag := flag.String("chip", "", "Supported EC chip name or alias (e.g. npcx99, npcx993, it81302, it81202, it82002, it82202)")
+ csvFlag := flag.String("csv", "", "Path to the EC pinout CSV file")
+ pinColFlag := flag.String("pin-col", "", "Optional: Column header containing pin numbers (e.g. 'NPCX99 (NPCX)' or 'IT82002 (ITE)')")
+ chipDataDirFlag := flag.String("chip-data-dir", "", "Optional: Path to directory containing custom chip JSON data files")
+ outputFlag := flag.String("output", "", "Path to the output DTSI file (defaults to stdout)")
+ flag.Parse()
+
+ if *chipDataDirFlag != "" {
+ if err := LoadExternalChipDir(*chipDataDirFlag); err != nil {
+ return fmt.Errorf("error loading chip data dir %s: %w", *chipDataDirFlag, err)
+ }
+ }
+
+ if *chipFlag == "" {
+ flag.Usage()
+ return fmt.Errorf("error: --chip is required. Available chips: %s", GetAvailableChips())
+ }
+
+ if *csvFlag == "" {
+ flag.Usage()
+ return fmt.Errorf("error: --csv is required")
+ }
+
+ chip, ok := GetChip(*chipFlag)
+ if !ok {
+ return fmt.Errorf("error: Unsupported chip %q. Available: %s", *chipFlag, GetAvailableChips())
+ }
+
+ csvFile, err := os.Open(*csvFlag)
+ if err != nil {
+ return fmt.Errorf("error opening CSV file: %w", err)
+ }
+ defer csvFile.Close()
+
+ configs, err := ParseCSV(csvFile, *pinColFlag)
+ if err != nil {
+ return fmt.Errorf("error parsing CSV: %w", err)
+ }
+
+ var out io.Writer = os.Stdout
+ var tmpFile *os.File
+ if *outputFlag != "" {
+ dir := filepath.Dir(*outputFlag)
+ var err error
+ tmpFile, err = os.CreateTemp(dir, ".tmp_generated_*.dtsi")
+ if err != nil {
+ return fmt.Errorf("error creating temp file: %w", err)
+ }
+ defer func() {
+ tmpFile.Close()
+ os.Remove(tmpFile.Name())
+ }()
+ out = tmpFile
+ }
+
+ if err := GenerateDTSI(out, configs, chip); err != nil {
+ return fmt.Errorf("error generating DTSI: %w", err)
+ }
+
+ if tmpFile != nil {
+ tmpFile.Close()
+ if err := os.Rename(tmpFile.Name(), *outputFlag); err != nil {
+ return fmt.Errorf("error saving output file: %w", err)
+ }
+ }
+
+ return nil
+}
diff --git a/ec_fw/main_test.go b/ec_fw/main_test.go
new file mode 100644
index 0000000..c598e4c
--- /dev/null
+++ b/ec_fw/main_test.go
@@ -0,0 +1,30 @@
+// Copyright 2026 The ChromiumOS Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+import (
+ "slices"
+ "strings"
+ "testing"
+)
+
+func TestGetAvailableChips(t *testing.T) {
+ chipsStr := GetAvailableChips()
+ chips := strings.Split(chipsStr, ", ")
+
+ if !slices.IsSorted(chips) {
+ t.Errorf("GetAvailableChips() returned unsorted list: %q", chipsStr)
+ }
+
+ expected := []string{
+ "it81202", "it81302", "it82002", "it82202", "it82202ax",
+ "npcx99", "npcx993", "npcx996", "npcx99*",
+ }
+ for _, name := range expected {
+ if !slices.Contains(chips, name) {
+ t.Errorf("Expected chip %q to be in available chips list: %q", name, chipsStr)
+ }
+ }
+}
diff --git a/ec_fw/parser.go b/ec_fw/parser.go
new file mode 100644
index 0000000..d91d6d2
--- /dev/null
+++ b/ec_fw/parser.go
@@ -0,0 +1,272 @@
+// Copyright 2026 The ChromiumOS Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+import (
+ "encoding/csv"
+ "fmt"
+ "io"
+ "strings"
+)
+
+type PinConfig struct {
+ PinNumber string
+ PinName string
+ NetName string
+ Pull string // UP, DOWN, N, ""
+ Dir string // IN, OUT, IO, ""
+ IO string // PWM, ADC, OD, TTL, UNUSED, ""
+ Enum string
+ PinTypeOverride string
+ InitialCondition string // HIGH, LOW, ""
+ NetNameOverride string
+ Voltage string // 1.8V, 3.3V, ""
+}
+
+// Column aliases for CSV parsing. The first entry in each slice is the canonical column name,
+// and the rest are alternative names that may appear in different CSV files.
+// Add variants to the list as needed to support different CSV formats.
+var columnAliases = map[string][]string{
+ "PinNumber": {"PinNumber", "Pin Number", "Pin", "Ball"},
+ "PinName": {"PinName", "Pin Name"},
+ "NetName": {"NetName", "Signal Name", "Net Name", "Net"},
+ "Dir": {"Dir", "DIR", "Direction"},
+ "IO": {"IO", "I/O Type", "IO Type", "I/O"},
+ "Pull": {"Pull", "Internal PU", "Pull Up", "Pull-Up"},
+ "Voltage": {"Voltage", "I/O Voltage", "Volts"},
+ "Enum": {"Enum", "Enum Name"},
+ "PinTypeOverride": {"PinTypeOverride", "Type", "SigType", "Pin Type Override"},
+ "InitialCondition": {"InitialCondition", "Initial Condition", "Init State", "Initial State"},
+ "NetNameOverride": {"NetNameOverride", "Net Name Override"},
+}
+
+// ParseCSV parses EC pinout CSV data from an io.Reader.
+func ParseCSV(reader io.Reader, pinCol string) ([]PinConfig, error) {
+ r := csv.NewReader(reader)
+ r.FieldsPerRecord = -1 // Allow variable number of fields per record
+
+ // Read header
+ header, err := r.Read()
+ if err != nil {
+ return nil, fmt.Errorf("failed to read header: %v", err)
+ }
+
+ findCol := func(candidates ...string) int {
+ normalize := func(s string) string {
+ return strings.Join(strings.Fields(s), " ")
+ }
+
+ // Pass 1: Exact match against header in original slice order
+ for _, cand := range candidates {
+ if cand == "" {
+ continue
+ }
+ normCand := normalize(cand)
+ for i, name := range header {
+ if strings.EqualFold(normalize(name), normCand) {
+ return i
+ }
+ }
+ }
+ // Pass 2: Substring match against header in original slice order
+ for _, cand := range candidates {
+ if cand == "" {
+ continue
+ }
+ normCand := strings.ToLower(normalize(cand))
+ for i, name := range header {
+ if strings.Contains(strings.ToLower(normalize(name)), normCand) {
+ return i
+ }
+ }
+ }
+ return -1
+ }
+
+ findColByAlias := func(key string, extraCandidates ...string) int {
+ candidates := columnAliases[key]
+ if len(extraCandidates) > 0 {
+ candidates = append(extraCandidates, candidates...)
+ }
+ return findCol(candidates...)
+ }
+
+ primaryPinIdx := findColByAlias("PinNumber", pinCol)
+ var fallbackPinIndices []int
+
+ if primaryPinIdx >= 0 {
+ primaryHeader := header[primaryPinIdx]
+ parts := strings.Split(primaryHeader, "(")
+ chipPrefix := strings.TrimSpace(parts[0])
+
+ for i, name := range header {
+ if i == primaryPinIdx {
+ continue
+ }
+ nameLower := strings.ToLower(name)
+ if (chipPrefix != "" && len(chipPrefix) > 2 && strings.Contains(nameLower, strings.ToLower(chipPrefix))) ||
+ (pinCol != "" && strings.Contains(nameLower, strings.ToLower(pinCol))) {
+ fallbackPinIndices = append(fallbackPinIndices, i)
+ }
+ }
+ }
+
+ netNameIdx := findColByAlias("NetName")
+ dirIdx := findColByAlias("Dir")
+ ioIdx := findColByAlias("IO")
+ pullIdx := findColByAlias("Pull")
+ voltageIdx := findColByAlias("Voltage")
+ enumIdx := findColByAlias("Enum")
+ pinTypeOverrideIdx := findColByAlias("PinTypeOverride")
+ initialCondIdx := findColByAlias("InitialCondition")
+ netNameOverrideIdx := findColByAlias("NetNameOverride")
+ pinNameIdx := findColByAlias("PinName")
+
+ if primaryPinIdx == -1 {
+ return nil, fmt.Errorf("missing required column for Pin Number (checked --pin-col=%q, 'PinNumber', 'Pin')", pinCol)
+ }
+ if netNameIdx == -1 {
+ return nil, fmt.Errorf("missing required column for Net Name (checked 'NetName', 'Signal Name')")
+ }
+
+ var configs []PinConfig
+ lineNum := 1
+
+ for {
+ row, err := r.Read()
+ lineNum++
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, fmt.Errorf("failed to read row at line %d: %v", lineNum, err)
+ }
+
+ getValue := func(idx int) string {
+ if idx >= 0 && idx < len(row) {
+ return strings.TrimSpace(row[idx])
+ }
+ return ""
+ }
+
+ netName := getValue(netNameIdx)
+ netUpper := strings.ToUpper(netName)
+ if netName == "" || netUpper == "NC" || netUpper == "NC (TP)" || netUpper == "NC(TP)" ||
+ netUpper == "NC_TP" || strings.HasPrefix(netUpper, "NC_") || strings.HasPrefix(netUpper, "NC ") ||
+ netUpper == "OPEN" || netUpper == "(OPEN)" || netUpper == "FLOATING" || netUpper == "(OPEN, FLOATING)" ||
+ netUpper == "N/A" || netUpper == "NA" {
+ continue
+ }
+
+ rawDir := getValue(dirIdx)
+ rawIO := getValue(ioIdx)
+ rawPull := getValue(pullIdx)
+ rawVoltage := getValue(voltageIdx)
+ enumVal := getValue(enumIdx)
+ sigType := getValue(pinTypeOverrideIdx)
+ initialCond := getValue(initialCondIdx)
+ netNameOverride := getValue(netNameOverrideIdx)
+ pinName := getValue(pinNameIdx)
+
+ pinNum := getValue(primaryPinIdx)
+ if pinNum == "" || pinNum == "N/A" || pinNum == "#N/A" || pinNum == "--" {
+ // Fall back to sibling variant columns for required peripheral or explicit enum signals
+ isPeripheralOrEnum := enumVal != "" || rawIO == "ADC" || rawIO == "I2C" || rawIO == "PWM" || rawIO == "TACH" ||
+ sigType == "ADC" || sigType == "I2C_CLOCK" || sigType == "I2C_DATA" || sigType == "PWM"
+ if isPeripheralOrEnum {
+ for _, fbIdx := range fallbackPinIndices {
+ val := getValue(fbIdx)
+ if val != "" && val != "N/A" && val != "#N/A" && val != "--" {
+ pinNum = val
+ break
+ }
+ }
+ }
+ }
+
+ if pinNum == "" || pinNum == "N/A" || pinNum == "#N/A" || pinNum == "--" {
+ continue
+ }
+
+ // Normalize Direction
+ direction := ""
+ if rawDir == "OUT" {
+ direction = "OUT"
+ } else if rawDir == "IN" {
+ direction = "IN"
+ } else if rawDir == "I/O" || rawDir == "IO" {
+ if strings.HasPrefix(sigType, "OUTPUT") || strings.HasPrefix(sigType, "ODR") {
+ direction = "OUT"
+ } else {
+ direction = "IN"
+ }
+ }
+
+ // Normalize Pull
+ pull := rawPull
+ if rawPull == "Y" {
+ pull = "UP"
+ } else if rawPull == "N" {
+ pull = "N"
+ }
+
+ // Normalize Voltage
+ voltage := rawVoltage
+ if strings.Contains(rawVoltage, "1.8") {
+ voltage = "1.8V"
+ } else if strings.Contains(rawVoltage, "3.3") {
+ voltage = "3.3V"
+ }
+
+ // Normalize Pin Type & Initial Condition
+ pinType := sigType
+ if pinType == "INPUT_PU" {
+ pinType = "INPUT"
+ pull = "UP"
+ if strings.HasSuffix(netName, "_L") || strings.HasSuffix(netName, "_ODL") || strings.HasSuffix(netName, "_LOW") {
+ pinType = "INPUT_L"
+ }
+ } else if pinType == "INPUT" && direction == "IN" {
+ if strings.HasSuffix(netName, "_L") || strings.HasSuffix(netName, "_ODL") || strings.HasSuffix(netName, "_LOW") {
+ pinType = "INPUT_L"
+ }
+ } else if pinType == "OUTPUT_L" && initialCond == "" {
+ initialCond = "LOW"
+ } else if pinType == "OUTPUT_H" && initialCond == "" {
+ initialCond = "HIGH"
+ } else if pinType == "OUTPUT_ODL" && initialCond == "" {
+ initialCond = "LOW"
+ } else if pinType == "OUTPUT_ODR" && initialCond == "" {
+ initialCond = "HIGH"
+ }
+
+ // Fix known signal defaults where ODR outputs are active high init in Zephyr
+ if netName == "EC_PCH_WAKE_ODL" && initialCond == "" {
+ initialCond = "HIGH"
+ }
+
+ // Clean double quotes in enum
+ enumVal = strings.ReplaceAll(enumVal, `""`, `"`)
+ enumVal = strings.Trim(enumVal, `"`)
+
+ config := PinConfig{
+ PinNumber: pinNum,
+ PinName: pinName,
+ NetName: netName,
+ Pull: pull,
+ Dir: direction,
+ IO: rawIO,
+ Enum: enumVal,
+ PinTypeOverride: pinType,
+ InitialCondition: initialCond,
+ NetNameOverride: netNameOverride,
+ Voltage: voltage,
+ }
+
+ configs = append(configs, config)
+ }
+
+ return configs, nil
+}
diff --git a/ec_fw/parser_test.go b/ec_fw/parser_test.go
new file mode 100644
index 0000000..3468617
--- /dev/null
+++ b/ec_fw/parser_test.go
@@ -0,0 +1,205 @@
+// Copyright 2026 The ChromiumOS Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestParseCSV_Valid(t *testing.T) {
+ csvData := `PinNumber,NetName,Pull,Dir,IO,Enum,PinTypeOverride,InitialCondition,NetNameOverride,Voltage
+A1,EC_WP_ODL,UP,IN,--,GPIO_WRITE_PROTECT_L,INPUT_L,,EC_WP_L,3.3V
+A2,GPIO_TEST,N,OUT,TTL,,OUTPUT_H,HIGH,,1.8V
+A3,EC_I2C_SENSOR_SCL,N,IO,OD,I2C_SENSOR_SCL,I2C_CLOCK,,,"3.3 V"
+`
+
+ configs, err := ParseCSV(strings.NewReader(csvData), "")
+ if err != nil {
+ t.Fatalf("ParseCSV failed: %v", err)
+ }
+
+ if len(configs) != 3 {
+ t.Fatalf("Expected 3 configs, got %d", len(configs))
+ }
+
+ c1 := configs[0]
+ if c1.PinNumber != "A1" || c1.NetName != "EC_WP_ODL" || c1.Pull != "UP" || c1.Dir != "IN" || c1.Enum != "GPIO_WRITE_PROTECT_L" || c1.PinTypeOverride != "INPUT_L" || c1.NetNameOverride != "EC_WP_L" || c1.Voltage != "3.3V" {
+ t.Errorf("Unexpected config 1: %+v", c1)
+ }
+
+ c2 := configs[1]
+ if c2.PinNumber != "A2" || c2.NetName != "GPIO_TEST" || c2.Pull != "N" || c2.Dir != "OUT" || c2.InitialCondition != "HIGH" {
+ t.Errorf("Unexpected config 2: %+v", c2)
+ }
+
+ c3 := configs[2]
+ expectedEnum := "I2C_SENSOR_SCL"
+ if c3.Enum != expectedEnum {
+ t.Errorf("Expected Enum %q, got %q", expectedEnum, c3.Enum)
+ }
+}
+
+func TestParseCSV_MultiVariantColumn(t *testing.T) {
+ csvData := `Signal Name,Subsystem,Description,DIR,Int,I/O Type,Internal PU,I/O Voltage,NPCX99 (NPCX),Type,Enum
+GSC_EC_PWR_BTN_ODL,GSC,Power Button input,IN,both,--,Y,3.30 V,E7,INPUT_PU,GPIO_POWER_BUTTON_L
+NC_UNCONNECTED,MISC,Unused pin,IN,no,--,N,1.80 V,F1,OTHER,
+EC_EDP_BL_EN_OD,DISP,Backlight enable,OUT,no,OD,N,3.30 V,E10,OUTPUT_ODR,GPIO_ENABLE_BACKLIGHT
+`
+
+ configs, err := ParseCSV(strings.NewReader(csvData), "NPCX")
+ if err != nil {
+ t.Fatalf("ParseCSV failed: %v", err)
+ }
+
+ if len(configs) != 2 {
+ t.Fatalf("Expected 2 configs (NC skipped), got %d", len(configs))
+ }
+
+ if configs[0].PinNumber != "E7" || configs[0].NetName != "GSC_EC_PWR_BTN_ODL" || configs[0].Pull != "UP" || configs[0].PinTypeOverride != "INPUT_L" {
+ t.Errorf("Unexpected config 0: %+v", configs[0])
+ }
+
+ if configs[1].PinNumber != "E10" || configs[1].NetName != "EC_EDP_BL_EN_OD" || configs[1].InitialCondition != "HIGH" {
+ t.Errorf("Unexpected config 1: %+v", configs[1])
+ }
+}
+
+func TestParseCSV_MissingRequiredColumn(t *testing.T) {
+ csvData := `PinName,Pull,Dir,Enum
+A1,UP,IN,GPIO_TEST
+`
+
+ _, err := ParseCSV(strings.NewReader(csvData), "")
+ if err == nil {
+ t.Error("Expected error for missing NetName column, got nil")
+ }
+}
+
+func TestParseCSV_CorruptAndEmptyInputs(t *testing.T) {
+ // Empty CSV Reader
+ _, err := ParseCSV(strings.NewReader(""), "")
+ if err == nil || !strings.Contains(err.Error(), "failed to read header") {
+ t.Errorf("Expected header read error for empty CSV, got %v", err)
+ }
+
+ // Broken CSV format (unmatched quote)
+ corruptCSV := "PinNumber,NetName\nA1,\"UNCLOSED_QUOTE\n"
+ _, err = ParseCSV(strings.NewReader(corruptCSV), "")
+ if err == nil || !strings.Contains(err.Error(), "failed to read row") {
+ t.Errorf("Expected row read error for corrupt CSV, got %v", err)
+ }
+
+ // Missing primary pin header with explicit pinCol
+ missingPinCSV := "HeaderNumber,NetName\nA1,EC_FOO\n"
+ _, err = ParseCSV(strings.NewReader(missingPinCSV), "SpecifiedCol")
+ if err == nil || !strings.Contains(err.Error(), "missing required column for Pin Number") {
+ t.Errorf("Expected missing column error, got %v", err)
+ }
+}
+
+func TestParseCSV_SignalFilteringExclusions(t *testing.T) {
+ csvData := `PinNumber,NetName
+A1,NC
+A2,NC (TP)
+A3,NC(TP)
+A4,NC_TP
+A5,NC_SPARE
+A6,NC 123
+A7,OPEN
+A8,(OPEN)
+A9,FLOATING
+B1,"(OPEN, FLOATING)"
+B2,N/A
+B3,NA
+B4,
+B5,VALID_SIGNAL
+`
+ configs, err := ParseCSV(strings.NewReader(csvData), "")
+ if err != nil {
+ t.Fatalf("ParseCSV failed: %v", err)
+ }
+ if len(configs) != 1 {
+ t.Fatalf("Expected only 1 valid config (VALID_SIGNAL), got %d (configs: %+v)", len(configs), configs)
+ }
+ if configs[0].NetName != "VALID_SIGNAL" || configs[0].PinNumber != "B5" {
+ t.Errorf("Unexpected remaining config: %+v", configs[0])
+ }
+}
+
+func TestParseCSV_FallbackPinNumberResolution(t *testing.T) {
+ csvData := `NPCX993 (Main),NPCX993 (Board),NetName,IO,Enum
+N/A,B3,EC_I2C_SCL,I2C,I2C_PORT_0
+N/A,B4,EC_GPIO_IN,IN,
+--,,EC_PWM_FAN,PWM,
+`
+ configs, err := ParseCSV(strings.NewReader(csvData), "Main")
+ if err != nil {
+ t.Fatalf("ParseCSV failed: %v", err)
+ }
+
+ // Primary N/A for peripheral I2C should fall back to Secondary B3
+ if len(configs) != 1 {
+ t.Fatalf("Expected 1 valid config via fallback, got %d", len(configs))
+ }
+ if configs[0].PinNumber != "B3" || configs[0].NetName != "EC_I2C_SCL" {
+ t.Errorf("Fallback failed, got config: %+v", configs[0])
+ }
+}
+
+func TestParseCSV_TypeAndConditionNormalizations(t *testing.T) {
+ csvData := `PinNumber,NetName,Dir,PinTypeOverride,InitialCondition
+A1,EC_WP_ODL,IN,INPUT_PU,
+A2,EC_SENSOR_LOW,IN,INPUT,
+A3,EC_RST_L,OUT,OUTPUT_L,
+A4,EC_POWER_ON,OUT,OUTPUT_H,
+A5,EC_PCH_WAKE_ODL,OUT,,
+`
+ configs, err := ParseCSV(strings.NewReader(csvData), "")
+ if err != nil {
+ t.Fatalf("ParseCSV failed: %v", err)
+ }
+ if len(configs) != 5 {
+ t.Fatalf("Expected 5 configs, got %d", len(configs))
+ }
+
+ // A1: INPUT_PU + _ODL -> INPUT_L, Pull UP
+ if configs[0].PinTypeOverride != "INPUT_L" || configs[0].Pull != "UP" {
+ t.Errorf("Config A1 mismatch: %+v", configs[0])
+ }
+ // A2: INPUT + _LOW -> INPUT_L
+ if configs[1].PinTypeOverride != "INPUT_L" {
+ t.Errorf("Config A2 mismatch: %+v", configs[1])
+ }
+ // A3: OUTPUT_L -> InitialCondition LOW
+ if configs[2].InitialCondition != "LOW" {
+ t.Errorf("Config A3 initial condition expected LOW, got %q", configs[2].InitialCondition)
+ }
+ // A4: OUTPUT_H -> InitialCondition HIGH
+ if configs[3].InitialCondition != "HIGH" {
+ t.Errorf("Config A4 initial condition expected HIGH, got %q", configs[3].InitialCondition)
+ }
+ // A5: EC_PCH_WAKE_ODL special case -> InitialCondition HIGH
+ if configs[4].InitialCondition != "HIGH" {
+ t.Errorf("Config A5 initial condition expected HIGH, got %q", configs[4].InitialCondition)
+ }
+}
+
+func TestParseCSV_HeaderNormalization(t *testing.T) {
+ csvData := `"NPCX9
+RVP",NetName
+A1,EC_GPIO_A1
+`
+ configs, err := ParseCSV(strings.NewReader(csvData), "NPCX9 RVP")
+ if err != nil {
+ t.Fatalf("ParseCSV failed: %v", err)
+ }
+ if len(configs) != 1 {
+ t.Fatalf("Expected 1 config, got %d", len(configs))
+ }
+ if configs[0].PinNumber != "A1" || configs[0].NetName != "EC_GPIO_A1" {
+ t.Errorf("Unexpected config: %+v", configs[0])
+ }
+}
diff --git a/ec_fw/test/ExampleEcPinout.csv b/ec_fw/test/ExampleEcPinout.csv
new file mode 100644
index 0000000..3cc8f2f
--- /dev/null
+++ b/ec_fw/test/ExampleEcPinout.csv
@@ -0,0 +1,21 @@
+Signal Name,DIR,I/O Type,Internal P,Initial Condition,Voltage,IT82002 (ITE),NPCX99 (NPCX),Type,Enum
+TEMP_SENSOR_1,IN,ADC,N,,,F13,D3,ADC,,
+TEMP_SENSOR_2,IN,ADC,N,,,F10,F2,ADC,,
+EC_I2C_PD1_PROG_SCL,OUT,OD,N,,,B3,C12,I2C,,
+EC_I2C_PD1_PROG_SDA,I/O,OD,N,,,B2,B12,I2C,,
+EC_KB_BL_PWM,OUT,PWM,N,,,N5,H10,PWM,,
+FAN_PWM_OD,OUT,PWM,N,,,M7,G9,PWM,,
+EC_FAN_TACH,IN,TACH,N,,,M11,E5,TACH,,
+EC_KSI_00,IN,--,Up,,,J12,A2,KEYBOARD,,
+KSI_01,IN,--,Up,,,J13,A3,KEYBOARD,,
+KSO_00,OUT,OD,N,,,M8,B5,KEYBOARD,,
+KSO_01,OUT,OD,N,,,J7,B6,KEYBOARD,,
+TABLET_MODE_L,IN,--,N,,,N3,H5,INPUT_L,,
+EC_VOLUP_BTN_ODL,IN,TTL,N,,,K6,G10,INPUT_L,,
+EC_VOLDN_BTN_ODL,IN,TTL,N,,,M5,G3,INPUT_L,,
+LID_OPEN,IN,TTL,N,,,N4,E6,INPUT,,
+CPU_C10_GATE_L,IN,TTL,N,,,E12,K11,INPUT_L,,
+EC_SOC_RSMRST_L,OUT,TTL,N,LOW,,D2,#N/A,OUTPUT_L,,
+EC_EN_EDP_BL,OUT,TTL,N,HIGH,,E1,J7,OUTPUT,,
+UART_EC_TX_GSC_DBG_RX,OUT,TTL,N,,,A3,H4,UART_TX,,
+UART_EC_RX_GSC_DBG_TX,IN,TTL,N,,,A4,G4,UART_RX,,
\ No newline at end of file
diff --git a/ec_fw/test/README.md b/ec_fw/test/README.md
new file mode 100644
index 0000000..f49146b
--- /dev/null
+++ b/ec_fw/test/README.md
@@ -0,0 +1,36 @@
+# `ec_fw` Test & Example Directory
+
+This directory contains reference example files for validating and testing the `ec_fw` EC Devicetree generator.
+
+---
+
+## Files
+
+- **`ExampleEcPinout.csv`**: Sample hardware EC pinout CSV spreadsheet containing signal names, direction, internal pull-ups/pull-downs, voltage levels, pin type overrides, enum names, and variant pinout columns (`NPCX99 (NPCX)`, `IT82002 (ITE)`).
+- **`generated.dtsi`**: Reference Devicetree overlay output generated from `ExampleEcPinout.csv` using the Nuvoton NPCX9 (`npcx99`) chip definition and `--pin-col="NPCX99 (NPCX)"`.
+
+---
+
+## Generation Commands
+
+### 1. Generate Reference Devicetree (`NPCX99` Variant - Nuvoton NPCX9)
+
+Run the following command from the repository root:
+
+```bash
+go run ./ec_fw --chip=npcx99 \
+ --csv=ec_fw/test/ExampleEcPinout.csv \
+ --pin-col="NPCX99 (NPCX)" \
+ --output=ec_fw/test/generated.dtsi
+```
+
+### 2. Generate Reference Devicetree (`IT82002` Variant - ITE IT82002)
+
+To generate for an ITE-based variant from the same spreadsheet:
+
+```bash
+go run ./ec_fw --chip=it82002 \
+ --csv=ec_fw/test/ExampleEcPinout.csv \
+ --pin-col="IT82002 (ITE)" \
+ --output=ec_fw/test/generated_ite.dtsi
+```
diff --git a/ec_fw/test/generated.dtsi b/ec_fw/test/generated.dtsi
new file mode 100644
index 0000000..36d2893
--- /dev/null
+++ b/ec_fw/test/generated.dtsi
@@ -0,0 +1,99 @@
+/* Copyright 2026 The ChromiumOS Authors
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ *
+ * This file is auto-generated - do not edit!
+ */
+
+/ {
+
+ named-adc-channels {
+ compatible = "named-adc-channels";
+
+ adc_temp_sensor_1: temp_sensor_1 {
+ io-channels = <&adc0 3>;
+ };
+ adc_temp_sensor_2: temp_sensor_2 {
+ io-channels = <&adc0 0>;
+ };
+ };
+
+ named-gpios {
+ compatible = "named-gpios";
+
+ gpio_cpu_c10_gate_l: cpu_c10_gate_l {
+ gpios = <&gpioa 5 (GPIO_INPUT | GPIO_ACTIVE_LOW)>;
+ };
+ gpio_ec_en_edp_bl: ec_en_edp_bl {
+ gpios = <&gpiob 7 (GPIO_OUTPUT | GPIO_ACTIVE_HIGH | GPIO_OUTPUT_INIT_HIGH)>;
+ };
+ gpio_ec_voldn_btn_odl: ec_voldn_btn_odl {
+ gpios = <&gpiof 1 (GPIO_INPUT | GPIO_ACTIVE_LOW)>;
+ };
+ gpio_ec_volup_btn_odl: ec_volup_btn_odl {
+ gpios = <&gpio5 0 (GPIO_INPUT | GPIO_ACTIVE_LOW)>;
+ };
+ gpio_lid_open: lid_open {
+ gpios = <&gpio0 1 (GPIO_INPUT | GPIO_ACTIVE_HIGH)>;
+ };
+ gpio_tablet_mode_l: tablet_mode_l {
+ gpios = <&gpio7 4 (GPIO_INPUT | GPIO_ACTIVE_LOW)>;
+ };
+ };
+
+ named-i2c-ports {
+ compatible = "named-i2c-ports";
+
+ i2c_ec_i2c_pd1_prog: ec_i2c_pd1_prog {
+ i2c-port = <&i2c0_0>;
+ };
+ };
+
+};
+
+&adc0 {
+ status = "okay";
+ pinctrl-0 = <
+ &adc0_chan0_gp45
+ &adc0_chan3_gp42
+ >;
+ pinctrl-names = "default";
+};
+
+&i2c0_0 {
+ status = "okay";
+ pinctrl-0 = <&i2c0_0_sda_scl_gpb4_b5>;
+ pinctrl-names = "default";
+};
+
+&pwm0 {
+ status = "okay";
+ pinctrl-0 = <&pwm0_gpc3>;
+ pinctrl-names = "default";
+};
+
+&pwm1 {
+ status = "okay";
+ pinctrl-0 = <&pwm1_gpc2>;
+ pinctrl-names = "default";
+};
+
+&tach1 {
+ status = "okay";
+ pinctrl-0 = <&ta1_1_in_gp40>;
+ pinctrl-names = "default";
+ port = <&gpioa>;
+};
+
+&kbd {
+ status = "okay";
+ pinctrl-0 = <
+ &ksi0_gp31
+ &ksi1_gp30
+ &kso00_gp21
+ &kso01_gp20
+ >;
+ pinctrl-names = "default";
+ row-size = <2>;
+ col-size = <2>;
+};
diff --git a/ec_fw/test/generated_ite.dtsi b/ec_fw/test/generated_ite.dtsi
new file mode 100644
index 0000000..ed0b9d4
--- /dev/null
+++ b/ec_fw/test/generated_ite.dtsi
@@ -0,0 +1,104 @@
+/* Copyright 2026 The ChromiumOS Authors
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ *
+ * This file is auto-generated - do not edit!
+ */
+
+/ {
+
+ named-adc-channels {
+ compatible = "named-adc-channels";
+
+ adc_temp_sensor_1: temp_sensor_1 {
+ io-channels = <&adc0 4>;
+ };
+ adc_temp_sensor_2: temp_sensor_2 {
+ io-channels = <&adc0 5>;
+ };
+ };
+
+ named-gpios {
+ compatible = "named-gpios";
+
+ gpio_cpu_c10_gate_l: cpu_c10_gate_l {
+ gpios = <&gpioj 0 (GPIO_INPUT | GPIO_ACTIVE_LOW)>;
+ };
+ gpio_ec_en_edp_bl: ec_en_edp_bl {
+ gpios = <&gpioc 6 (GPIO_OUTPUT | GPIO_ACTIVE_HIGH | GPIO_OUTPUT_INIT_HIGH)>;
+ };
+ gpio_ec_soc_rsmrst_l: ec_soc_rsmrst_l {
+ gpios = <&gpiob 2 (GPIO_OUTPUT | GPIO_ACTIVE_LOW | GPIO_OUTPUT_INIT_LOW)>;
+ };
+ gpio_ec_voldn_btn_odl: ec_voldn_btn_odl {
+ gpios = <&gpioa 0 (GPIO_INPUT | GPIO_ACTIVE_LOW)>;
+ };
+ gpio_ec_volup_btn_odl: ec_volup_btn_odl {
+ gpios = <&gpioa 4 (GPIO_INPUT | GPIO_ACTIVE_LOW)>;
+ };
+ gpio_lid_open: lid_open {
+ gpios = <&gpiod 3 (GPIO_INPUT | GPIO_ACTIVE_HIGH)>;
+ };
+ gpio_tablet_mode_l: tablet_mode_l {
+ gpios = <&gpiod 1 (GPIO_INPUT | GPIO_ACTIVE_LOW)>;
+ };
+ };
+
+ named-i2c-ports {
+ compatible = "named-i2c-ports";
+
+ i2c_ec_i2c_pd1_prog: ec_i2c_pd1_prog {
+ i2c-port = <&i2c1>;
+ };
+ };
+
+};
+
+&adc0 {
+ status = "okay";
+ pinctrl-0 = <
+ &adc0_ch4_gpi4_default
+ &adc0_ch5_gpi5_default
+ >;
+ pinctrl-names = "default";
+};
+
+&i2c1 {
+ status = "okay";
+ pinctrl-0 = <
+ &i2c1_clk_gpc1_default
+ &i2c1_data_gpc2_default
+ >;
+ pinctrl-names = "default";
+};
+
+&pwm1 {
+ status = "okay";
+ pinctrl-0 = <&pwm1_gpa1_default>;
+ pinctrl-names = "default";
+};
+
+&pwm6 {
+ status = "okay";
+ pinctrl-0 = <&pwm6_gpa6_default>;
+ pinctrl-names = "default";
+};
+
+&tach0 {
+ status = "okay";
+ pinctrl-0 = <&tach0a_gpd6_default>;
+ pinctrl-names = "default";
+};
+
+&kbd {
+ status = "okay";
+ pinctrl-0 = <
+ &ksi0_default
+ &ksi1_default
+ &kso0_default
+ &kso1_default
+ >;
+ pinctrl-names = "default";
+ row-size = <2>;
+ col-size = <2>;
+};
diff --git a/ec_fw/test/generated_npcx.dtsi b/ec_fw/test/generated_npcx.dtsi
new file mode 100644
index 0000000..36d2893
--- /dev/null
+++ b/ec_fw/test/generated_npcx.dtsi
@@ -0,0 +1,99 @@
+/* Copyright 2026 The ChromiumOS Authors
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ *
+ * This file is auto-generated - do not edit!
+ */
+
+/ {
+
+ named-adc-channels {
+ compatible = "named-adc-channels";
+
+ adc_temp_sensor_1: temp_sensor_1 {
+ io-channels = <&adc0 3>;
+ };
+ adc_temp_sensor_2: temp_sensor_2 {
+ io-channels = <&adc0 0>;
+ };
+ };
+
+ named-gpios {
+ compatible = "named-gpios";
+
+ gpio_cpu_c10_gate_l: cpu_c10_gate_l {
+ gpios = <&gpioa 5 (GPIO_INPUT | GPIO_ACTIVE_LOW)>;
+ };
+ gpio_ec_en_edp_bl: ec_en_edp_bl {
+ gpios = <&gpiob 7 (GPIO_OUTPUT | GPIO_ACTIVE_HIGH | GPIO_OUTPUT_INIT_HIGH)>;
+ };
+ gpio_ec_voldn_btn_odl: ec_voldn_btn_odl {
+ gpios = <&gpiof 1 (GPIO_INPUT | GPIO_ACTIVE_LOW)>;
+ };
+ gpio_ec_volup_btn_odl: ec_volup_btn_odl {
+ gpios = <&gpio5 0 (GPIO_INPUT | GPIO_ACTIVE_LOW)>;
+ };
+ gpio_lid_open: lid_open {
+ gpios = <&gpio0 1 (GPIO_INPUT | GPIO_ACTIVE_HIGH)>;
+ };
+ gpio_tablet_mode_l: tablet_mode_l {
+ gpios = <&gpio7 4 (GPIO_INPUT | GPIO_ACTIVE_LOW)>;
+ };
+ };
+
+ named-i2c-ports {
+ compatible = "named-i2c-ports";
+
+ i2c_ec_i2c_pd1_prog: ec_i2c_pd1_prog {
+ i2c-port = <&i2c0_0>;
+ };
+ };
+
+};
+
+&adc0 {
+ status = "okay";
+ pinctrl-0 = <
+ &adc0_chan0_gp45
+ &adc0_chan3_gp42
+ >;
+ pinctrl-names = "default";
+};
+
+&i2c0_0 {
+ status = "okay";
+ pinctrl-0 = <&i2c0_0_sda_scl_gpb4_b5>;
+ pinctrl-names = "default";
+};
+
+&pwm0 {
+ status = "okay";
+ pinctrl-0 = <&pwm0_gpc3>;
+ pinctrl-names = "default";
+};
+
+&pwm1 {
+ status = "okay";
+ pinctrl-0 = <&pwm1_gpc2>;
+ pinctrl-names = "default";
+};
+
+&tach1 {
+ status = "okay";
+ pinctrl-0 = <&ta1_1_in_gp40>;
+ pinctrl-names = "default";
+ port = <&gpioa>;
+};
+
+&kbd {
+ status = "okay";
+ pinctrl-0 = <
+ &ksi0_gp31
+ &ksi1_gp30
+ &kso00_gp21
+ &kso01_gp20
+ >;
+ pinctrl-names = "default";
+ row-size = <2>;
+ col-size = <2>;
+};
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..e794141
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module firmweaver
+
+go 1.26.4