firmweaver: update dtsi generation

Support unmodified EC pinout sheets and match arbitrage output.

BUG=b:427556454
TEST=go test -v -race ./...
TEST=compared generated kenai dtsi between arb and firmweaver
Change-Id: Id7c95bb62edc558472b774f19303ad5466e2474c
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/firmweaver/+/8522577
Reviewed-by: Karthik Ramasubramanian <kramasub@google.com>
Commit-Queue: Kshitij Shah <tij@google.com>
Tested-by: Kshitij Shah <tij@google.com>
diff --git a/zephyr_dtsi/chips.go b/zephyr_dtsi/chips.go
index a9cf881..1d65c6c 100644
--- a/zephyr_dtsi/chips.go
+++ b/zephyr_dtsi/chips.go
@@ -31,6 +31,7 @@
 	iteSda        = regexp.MustCompile(`SMDAT([0-9]+[A-Z]*)`)
 	npcxTachRegex = regexp.MustCompile(`^T([A-Z])([0-9])$`)
 	iteTachRegex  = regexp.MustCompile(`^TACH([0-9])([A-Z])$`)
+	iteSoutRegex  = regexp.MustCompile(`^SOUT([0-9]+)$`)
 )
 
 type ChipData struct {
@@ -219,6 +220,25 @@
 	return 0, "", fmt.Errorf("pin %s (%s) does not support TACH", pinNum, pinDef)
 }
 
+// ResolveITEUART returns the 0-based SOUT index (e.g. 0 for SOUT0, 1 for SOUT1) if the pin supports ITE UART TX.
+func (c *Chip) ResolveITEUART(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)
+	}
+	for _, part := range strings.Split(pinDef, "/") {
+		part = strings.TrimSpace(part)
+		if m := iteSoutRegex.FindStringSubmatch(part); m != nil {
+			idx, err := strconv.Atoi(m[1])
+			if err != nil {
+				return 0, fmt.Errorf("invalid SOUT index %q in %s: %v", m[1], pinDef, err)
+			}
+			return idx, nil
+		}
+	}
+	return 0, fmt.Errorf("pin %s (%s) does not support UART TX", pinNum, pinDef)
+}
+
 var chips = map[string]*Chip{}
 
 // GetChip returns the registered Chip by name or alias.
diff --git a/zephyr_dtsi/chips_test.go b/zephyr_dtsi/chips_test.go
index f2db0b2..0383778 100644
--- a/zephyr_dtsi/chips_test.go
+++ b/zephyr_dtsi/chips_test.go
@@ -348,3 +348,32 @@
 		}
 	}
 }
+
+func TestChips_ResolveITEUART(t *testing.T) {
+	ite := getChipOrFatal(t, "it82002")
+	npcx := getChipOrFatal(t, "npcx99")
+
+	tests := []struct {
+		chip         *Chip
+		pinNum       string
+		expectedPort int
+		expectErr    bool
+	}{
+		{ite, "A3", 0, false},  // SOUT0/GPB1
+		{ite, "B11", 1, false}, // SOUT1/PS2DAT0/GPF1
+		{ite, "A4", 0, true},   // SIN0/GPB0 (RX, not SOUT)
+		{ite, "Z99", 0, true},  // Pin not found
+		{npcx, "F4", 0, true},  // Not an ITE chip
+	}
+
+	for _, tt := range tests {
+		port, err := tt.chip.ResolveITEUART(tt.pinNum)
+		if (err != nil) != tt.expectErr {
+			t.Errorf("%s ResolveITEUART(%s) error status: expected err=%t, got %v", tt.chip.Name, tt.pinNum, tt.expectErr, err)
+			continue
+		}
+		if !tt.expectErr && port != tt.expectedPort {
+			t.Errorf("%s ResolveITEUART(%s) expected port=%d, got %d", tt.chip.Name, tt.pinNum, tt.expectedPort, port)
+		}
+	}
+}
diff --git a/zephyr_dtsi/generator.go b/zephyr_dtsi/generator.go
index 030ef17..27a8708 100644
--- a/zephyr_dtsi/generator.go
+++ b/zephyr_dtsi/generator.go
@@ -9,6 +9,7 @@
 	"fmt"
 	"io"
 	"maps"
+	"os"
 	"regexp"
 	"slices"
 	"strconv"
@@ -31,6 +32,13 @@
 	enum   string
 }
 
+type unusedPin struct {
+	comment string
+	port    string
+	pinNum  int
+	flags   string
+}
+
 type adcPin struct {
 	label   string
 	pin     string
@@ -46,20 +54,29 @@
 	enums    []string
 }
 
+type iteUartNode struct {
+	rxGP string
+	txGP string
+}
+
 // dtsiGenerator holds the state for generating the DTSI file.
 type dtsiGenerator struct {
 	w           io.Writer
 	chip        *Chip
 	gpios       []gpioPin
+	unusedPins  []unusedPin
 	adcs        []adcPin
 	i2cPorts    []i2cPort
 	i2cEnables  map[string][]string
+	i2cCtrls    map[string]bool
 	pwmEnables  map[int]string
 	adcEnabled  bool
 	adcPinctrls []string
 	tachEnables map[int]string
 	tachPorts   map[int]string
+	iteUarts    map[int]iteUartNode
 	kbPins      []string
+	kbSleepPins []string
 	maxKsi      int
 	maxKso      int
 }
@@ -69,9 +86,11 @@
 		w:           w,
 		chip:        chip,
 		i2cEnables:  make(map[string][]string),
+		i2cCtrls:    make(map[string]bool),
 		pwmEnables:  make(map[int]string),
 		tachEnables: make(map[int]string),
 		tachPorts:   make(map[int]string),
+		iteUarts:    make(map[int]iteUartNode),
 		maxKsi:      -1,
 		maxKso:      -1,
 	}
@@ -88,7 +107,7 @@
 		}
 		processedPins[cfg.PinNumber] = true
 
-		if cfg.NetName == "" {
+		if cfg.NetName == "" && cfg.PinTypeOverride != "UNUSED_IN" && cfg.PinTypeOverride != "UNUSED_OUT" {
 			continue
 		}
 
@@ -99,7 +118,7 @@
 		netName = cleanNetName(netName)
 
 		// Check excluded net names
-		if strings.HasPrefix(netName, "ESPI_") || strings.HasPrefix(netName, "UART_") || netName == "EC_RST_ODL" {
+		if strings.HasPrefix(netName, "ESPI_") || strings.HasSuffix(netName, "EC_RST_ODL") {
 			continue
 		}
 
@@ -113,6 +132,18 @@
 			gpName = fmt.Sprintf("gp%s%d", port, pinNum)
 		}
 
+		if cfg.PinTypeOverride == "UNUSED_IN" || cfg.PinTypeOverride == "UNUSED_OUT" {
+			if gpioErr != nil {
+				if def, ok := chip.PinMap[cfg.PinNumber]; ok {
+					fmt.Fprintf(os.Stderr, "Warning: skipping %s: pin %s (%s) has no GPIO function\n", netName, cfg.PinNumber, def)
+					continue
+				}
+				return fmt.Errorf("failed to resolve GPIO for pin %s: %v", cfg.PinNumber, gpioErr)
+			}
+			g.processUnusedPin(cfg, netName, port, pinNum)
+			continue
+		}
+
 		// Try processing as Keyboard matrix
 		if g.tryProcessKeyboard(cfg, netName, gpName) {
 			continue
@@ -154,9 +185,22 @@
 			continue
 		}
 
+		// Try processing as UART
+		processedUART, err := g.tryProcessUART(cfg, netName, gpName)
+		if err != nil {
+			return err
+		}
+		if processedUART || strings.HasPrefix(netName, "UART_") {
+			continue
+		}
+
 		// Process as regular GPIO
 		if isGPIO(cfg) {
 			if gpioErr != nil {
+				if def, ok := chip.PinMap[cfg.PinNumber]; ok {
+					fmt.Fprintf(os.Stderr, "Warning: skipping %s: pin %s (%s) has no GPIO function\n", netName, cfg.PinNumber, def)
+					continue
+				}
 				return fmt.Errorf("failed to resolve GPIO for pin %s: %v", cfg.PinNumber, gpioErr)
 			}
 			g.processGPIO(cfg, netName, port, pinNum, gpName)
@@ -205,9 +249,11 @@
 	if g.chip.IsNPCX() {
 		if gpName != "" {
 			g.kbPins = append(g.kbPins, fmt.Sprintf("&%s%s_%s", prefix, idx, gpName))
+			g.kbSleepPins = append(g.kbSleepPins, fmt.Sprintf("&%s%s_gpio_%s", prefix, idx, gpName))
 		}
 	} else {
 		g.kbPins = append(g.kbPins, fmt.Sprintf("&%s%s_default", prefix, idx))
+		g.kbSleepPins = append(g.kbSleepPins, fmt.Sprintf("&%s%s_sleep", prefix, idx))
 	}
 	return true
 }
@@ -286,7 +332,7 @@
 }
 
 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")
+	isI2C := cfg.IO == "I2C" || cfg.PinTypeOverride == "I2C_CLOCK" || cfg.PinTypeOverride == "I2C_DATA" || cfg.PinTypeOverride == "I2C"
 	if !isI2C {
 		return false, nil
 	}
@@ -341,6 +387,7 @@
 		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}
+		g.i2cCtrls["i2c_ctrl"+i2cChan] = true
 	} else {
 		// ITE
 		sdaGP, err := findITESDAGPIO(g.chip, cfg.PinNumber)
@@ -356,6 +403,40 @@
 	return true, nil
 }
 
+func (g *dtsiGenerator) tryProcessUART(cfg PinConfig, netName, gpName string) (bool, error) {
+	if cfg.PinTypeOverride == "UART_RX" {
+		return true, nil
+	}
+	if cfg.PinTypeOverride != "UART_TX" {
+		return false, nil
+	}
+	if g.chip.IsNPCX() {
+		def, ok := g.chip.PinMap[cfg.PinNumber]
+		if !ok {
+			return false, fmt.Errorf("failed to resolve UART for pin %s: pin %s not found in chip %s", cfg.PinNumber, cfg.PinNumber, g.chip.Name)
+		}
+		if !hasFunction(def, "CR_SOUT1") {
+			fmt.Fprintf(os.Stderr, "Warning: skipping %s: NPCX only supports default uart1 (CR_SOUT1), pin %s is %s\n", netName, cfg.PinNumber, def)
+		}
+		return true, nil
+	}
+	soutIdx, err := g.chip.ResolveITEUART(cfg.PinNumber)
+	if err != nil {
+		return false, fmt.Errorf("failed to resolve UART for pin %s: %v", cfg.PinNumber, err)
+	}
+	uartNum := soutIdx + 1
+	if uartNum == 1 {
+		g.iteUarts[1] = iteUartNode{}
+		return true, nil
+	}
+	rxGP, err := findFuncGPIO(g.chip, fmt.Sprintf("SIN%d", soutIdx))
+	if err != nil {
+		return false, fmt.Errorf("failed to find ITE UART RX GPIO for pin %s: %v", cfg.PinNumber, err)
+	}
+	g.iteUarts[uartNum] = iteUartNode{rxGP: rxGP, txGP: gpName}
+	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))
@@ -371,6 +452,15 @@
 	}
 }
 
+func (g *dtsiGenerator) processUnusedPin(cfg PinConfig, netName, port string, pinNum int) {
+	g.unusedPins = append(g.unusedPins, unusedPin{
+		comment: netName,
+		port:    port,
+		pinNum:  pinNum,
+		flags:   determineUnusedFlags(cfg),
+	})
+}
+
 func (g *dtsiGenerator) write() error {
 	ew := &errWriter{w: g.w}
 	g.w = ew
@@ -379,6 +469,9 @@
 	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.SortStableFunc(g.unusedPins, func(a, b unusedPin) int {
+		return cmp.Or(cmp.Compare(a.port, b.port), cmp.Compare(a.pinNum, b.pinNum))
+	})
 	slices.Sort(g.adcPinctrls)
 
 	// Write header
@@ -443,6 +536,25 @@
 		fmt.Fprintln(g.w)
 	}
 
+	// Write Unused pins
+	if len(g.unusedPins) > 0 {
+		fmt.Fprintln(g.w, "\tunused-pins {")
+		fmt.Fprintln(g.w, "\t\tcompatible = \"unused-gpios\";")
+		fmt.Fprintln(g.w, "\t\tunused-gpios =")
+		for i, up := range g.unusedPins {
+			if up.comment != "" {
+				fmt.Fprintf(g.w, "\t\t\t/* %s */\n", up.comment)
+			}
+			sep := ","
+			if i == len(g.unusedPins)-1 {
+				sep = ";"
+			}
+			fmt.Fprintf(g.w, "\t\t\t<&gpio%s %d %s>%s\n", up.port, up.pinNum, up.flags, sep)
+		}
+		fmt.Fprintln(g.w, "\t};")
+		fmt.Fprintln(g.w)
+	}
+
 	fmt.Fprintln(g.w, "};")
 	fmt.Fprintln(g.w)
 
@@ -486,6 +598,16 @@
 		fmt.Fprintln(g.w)
 	}
 
+	i2cCtrls := slices.Collect(maps.Keys(g.i2cCtrls))
+	slices.Sort(i2cCtrls)
+
+	for _, ctrlNode := range i2cCtrls {
+		fmt.Fprintf(g.w, "&%s {\n", ctrlNode)
+		fmt.Fprintln(g.w, "\tstatus = \"okay\";")
+		fmt.Fprintln(g.w, "};")
+		fmt.Fprintln(g.w)
+	}
+
 	// Write PWM pinctrl
 	pwmChans := slices.Collect(maps.Keys(g.pwmEnables))
 	slices.Sort(pwmChans)
@@ -504,19 +626,49 @@
 	slices.Sort(tachChans)
 
 	for _, ch := range tachChans {
+		portLetter := strings.ToUpper(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\";")
 		if g.chip.IsNPCX() {
-			portLetter := g.tachPorts[ch]
-			fmt.Fprintf(g.w, "&tach%d {\n", ch)
+			fmt.Fprintf(g.w, "\tport = <NPCX_TACH_PORT_%s>;\n", portLetter)
+			fmt.Fprintln(g.w, "\tsample-clk = <NPCX_TACH_FREQ_LFCLK>;")
+			fmt.Fprintln(g.w, "\tpulses-per-round = <2>;")
+		} else {
+			fmt.Fprintf(g.w, "\tchannel = <IT8XXX2_TACH_CHANNEL_%s>;\n", portLetter)
+			fmt.Fprintln(g.w, "\tpulses-per-round = <2>;")
+		}
+		fmt.Fprintln(g.w, "};")
+		fmt.Fprintln(g.w)
+	}
+
+	// Write UART pinctrl
+	if _, hasUart1 := g.iteUarts[1]; len(g.iteUarts) > 0 && !hasUart1 {
+		fmt.Fprintln(g.w, "&uart1 {")
+		fmt.Fprintln(g.w, "\tstatus = \"disabled\";")
+		fmt.Fprintln(g.w, "};")
+		fmt.Fprintln(g.w)
+		fmt.Fprintln(g.w, "&ite_uart1_wrapper {")
+		fmt.Fprintln(g.w, "\tstatus = \"disabled\";")
+		fmt.Fprintln(g.w, "};")
+		fmt.Fprintln(g.w)
+
+		uartNums := slices.Collect(maps.Keys(g.iteUarts))
+		slices.Sort(uartNums)
+		for _, num := range uartNums {
+			u := g.iteUarts[num]
+			fmt.Fprintf(g.w, "&uart%d {\n", num)
 			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, "\tcurrent-speed = <115200>;")
 			fmt.Fprintln(g.w, "};")
 			fmt.Fprintln(g.w)
-		} else {
-			fmt.Fprintf(g.w, "&tach%d {\n", ch)
+			fmt.Fprintf(g.w, "&ite_uart%d_wrapper {\n", num)
 			fmt.Fprintln(g.w, "\tstatus = \"okay\";")
-			fmt.Fprintf(g.w, "\tpinctrl-0 = <%s>;\n", g.tachEnables[ch])
+			fmt.Fprintln(g.w, "\tpinctrl-0 = <")
+			fmt.Fprintf(g.w, "\t\t&uart%d_rx_%s_default\n", num, u.rxGP)
+			fmt.Fprintf(g.w, "\t\t&uart%d_tx_%s_default\n", num, u.txGP)
+			fmt.Fprintln(g.w, "\t>;")
 			fmt.Fprintln(g.w, "\tpinctrl-names = \"default\";")
 			fmt.Fprintln(g.w, "};")
 			fmt.Fprintln(g.w)
@@ -526,6 +678,7 @@
 	// Write Keyboard matrix pinctrl
 	if len(g.kbPins) > 0 {
 		slices.Sort(g.kbPins)
+		slices.Sort(g.kbSleepPins)
 		fmt.Fprintln(g.w, "&kbd {")
 		fmt.Fprintln(g.w, "\tstatus = \"okay\";")
 		fmt.Fprintln(g.w, "\tpinctrl-0 = <")
@@ -533,7 +686,12 @@
 			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, "\tpinctrl-1 = <")
+		for _, p := range g.kbSleepPins {
+			fmt.Fprintf(g.w, "\t\t%s\n", p)
+		}
+		fmt.Fprintln(g.w, "\t>;")
+		fmt.Fprintln(g.w, "\tpinctrl-names = \"default\", \"sleep\";")
 		rowSize := 0
 		if g.maxKsi >= 0 {
 			rowSize = g.maxKsi + 1
@@ -550,7 +708,7 @@
 	return ew.err
 }
 
-func findSDAGPIO(chip *Chip, sdaFunc string) (string, error) {
+func findFuncGPIO(chip *Chip, fn string) (string, error) {
 	var pins []string
 	if len(chip.PkgPins) > 0 {
 		pins = slices.Clone(chip.PkgPins)
@@ -561,7 +719,7 @@
 
 	for _, pinNum := range pins {
 		def := chip.PinMap[pinNum]
-		if hasFunction(def, sdaFunc) {
+		if hasFunction(def, fn) {
 			port, pin, err := chip.ResolveGPIO(pinNum)
 			if err != nil {
 				return "", err
@@ -569,7 +727,7 @@
 			return fmt.Sprintf("gp%s%d", port, pin), nil
 		}
 	}
-	return "", fmt.Errorf("matching SDA pin function %s not found in chip map", sdaFunc)
+	return "", fmt.Errorf("matching pin function %s not found in chip map", fn)
 }
 
 func findNPCXSDAGPIO(chip *Chip, sclPinNum string) (string, error) {
@@ -579,7 +737,7 @@
 		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)
+	return findFuncGPIO(chip, sdaFunc)
 }
 
 func findITESDAGPIO(chip *Chip, sclPinNum string) (string, error) {
@@ -589,7 +747,7 @@
 		return "", fmt.Errorf("not an ITE SCL pin definition: %s", sclDef)
 	}
 	sdaFunc := fmt.Sprintf("SMDAT%s", m[1])
-	return findSDAGPIO(chip, sdaFunc)
+	return findFuncGPIO(chip, sdaFunc)
 }
 
 func extractNPCXI2CIndex(pinDef string) int {
@@ -608,6 +766,9 @@
 }
 
 func isGPIO(cfg PinConfig) bool {
+	if cfg.IO == "UNUSED" || cfg.PinTypeOverride == "UNUSED" {
+		return false
+	}
 	if cfg.Dir == "IN" || cfg.Dir == "OUT" {
 		return true
 	}
@@ -618,6 +779,29 @@
 	return false
 }
 
+func determineUnusedFlags(cfg PinConfig) string {
+	var flags []string
+	isOutput := cfg.PinTypeOverride == "UNUSED_OUT"
+	if isOutput {
+		flags = append(flags, "GPIO_OUTPUT", "GPIO_ACTIVE_HIGH")
+	} else {
+		flags = append(flags, "GPIO_INPUT")
+	}
+	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")
+		}
+	}
+	return "(" + strings.Join(flags, " | ") + ")"
+}
+
 func hasFunction(pinDef, function string) bool {
 	parts := strings.Split(pinDef, "/")
 	for _, part := range parts {
diff --git a/zephyr_dtsi/generator_test.go b/zephyr_dtsi/generator_test.go
index aa304cc..e647f9e 100644
--- a/zephyr_dtsi/generator_test.go
+++ b/zephyr_dtsi/generator_test.go
@@ -330,19 +330,12 @@
 			t.Fatalf("GenerateDTSI failed: %v", err)
 		}
 
-		output := buf.String()
-		requiredNodes := []string{
-			"named-adc-channels",
-			"named-gpios",
-			"named-i2c-ports",
-			"&adc0",
-			"&pwm0",
+		expected, err := os.ReadFile("test/generated_npcx.dtsi")
+		if err != nil {
+			t.Fatalf("Failed to read test/generated_npcx.dtsi: %v", err)
 		}
-
-		for _, req := range requiredNodes {
-			if !strings.Contains(output, req) {
-				t.Errorf("Generated DTSI missing expected node structure %q", req)
-			}
+		if buf.String() != string(expected) {
+			t.Errorf("Generated NPCX99 DTSI differs from test/generated_npcx.dtsi.\nGot:\n%s\nWant:\n%s", buf.String(), string(expected))
 		}
 	})
 
@@ -362,23 +355,12 @@
 			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",
+		expected, err := os.ReadFile("test/generated_ite.dtsi")
+		if err != nil {
+			t.Fatalf("Failed to read test/generated_ite.dtsi: %v", err)
 		}
-
-		for _, req := range requiredNodes {
-			if !strings.Contains(output, req) {
-				t.Errorf("Generated DTSI missing expected node structure %q", req)
-			}
+		if buf.String() != string(expected) {
+			t.Errorf("Generated IT82002 DTSI differs from test/generated_ite.dtsi.\nGot:\n%s\nWant:\n%s", buf.String(), string(expected))
 		}
 	})
 }
@@ -419,3 +401,190 @@
 		t.Error("Expected D5 to NOT be processed as GPIO because PinTypeOverride is INVALID_GPIO")
 	}
 }
+
+func TestGenerateDTSI_NonGPIOHardwarePinsAndUnusedPins(t *testing.T) {
+	chip := &Chip{
+		Name:   "ite-mock",
+		Vendor: "ite",
+		PinMap: map[string]string{
+			"L1": "WRST#",
+			"G6": "VCORE",
+			"K6": "VCC1_RST#/GPO77",
+			"M5": "PWM0/GPA0",
+			"A6": "FMISO/GPG5",
+			"M7": "PWM6/SSCK/GPA6",
+		},
+	}
+
+	configs := []PinConfig{
+		{PinNumber: "L1", NetName: "EC_ERST_L", Dir: "IN", PinTypeOverride: "INPUT_L"},
+		{PinNumber: "G6", NetName: "VCORE_CAP", PinTypeOverride: "UNUSED_IN"},
+		{PinNumber: "K6", NetName: "PLT_EC_RST_ODL", Dir: "IN", PinTypeOverride: "INPUT_L"},
+		{PinNumber: "A6", NetName: "EN_PP5000_USBA_VBUS", Dir: "OUT", PinTypeOverride: "UNUSED_OUT", InitialCondition: "LOW", Pull: "DOWN"},
+		{PinNumber: "M7", NetName: "NC", PinTypeOverride: "UNUSED_IN", Pull: "UP"},
+		{PinNumber: "M5", NetName: "EC_VOLDN_BTN_ODL", PinTypeOverride: "UNUSED_IN"},
+	}
+
+	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_erst_l") || strings.Contains(out, "plt_ec_rst_odl") || strings.Contains(out, "en_pp5000_usba_vbus") {
+		t.Errorf("Expected non-GPIO hardware pins, *EC_RST_ODL, and UNUSED_OUT to be excluded from named-gpios:\n%s", out)
+	}
+
+	expectedUnused := strings.Join([]string{
+		"\tunused-pins {",
+		"\t\tcompatible = \"unused-gpios\";",
+		"\t\tunused-gpios =",
+		"\t\t\t/* EC_VOLDN_BTN_ODL */",
+		"\t\t\t<&gpioa 0 (GPIO_INPUT)>,",
+		"\t\t\t/* NC */",
+		"\t\t\t<&gpioa 6 (GPIO_INPUT | GPIO_PULL_UP)>,",
+		"\t\t\t/* EN_PP5000_USBA_VBUS */",
+		"\t\t\t<&gpiog 5 (GPIO_OUTPUT | GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN | GPIO_OUTPUT_INIT_LOW)>;",
+		"\t};",
+	}, "\n")
+	if !strings.Contains(out, expectedUnused) {
+		t.Errorf("Expected unused-pins block:\n%s\nGot:\n%s", expectedUnused, out)
+	}
+
+	// Unknown pin not in chip.PinMap should still fail
+	badConfigs := []PinConfig{{PinNumber: "Z99", NetName: "BAD_PIN", Dir: "IN"}}
+	buf.Reset()
+	if err := GenerateDTSI(&buf, badConfigs, chip); err == nil || !strings.Contains(err.Error(), "failed to resolve GPIO for pin Z99") {
+		t.Errorf("Expected error for unknown pin Z99, got %v", err)
+	}
+}
+
+func TestGenerateDTSI_PeripheralParity(t *testing.T) {
+	t.Run("NPCX_KbdSleep_Tach_I2CCtrl", func(t *testing.T) {
+		chip := &Chip{
+			Name:   "npcx-mock",
+			Vendor: "npcx",
+			PinMap: map[string]string{
+				"B2": "GPIOB4/I2C0_SDA0",
+				"B3": "GPIOB5/I2C0_SCL0",
+				"F5": "GPIOF5/TB1",
+				"G1": "KSI0/GPIO31",
+				"G2": "KSO01/GPIO21",
+			},
+		}
+		configs := []PinConfig{
+			// I2C_CLOCK without IO="OD" (e.g. Fatcat NPCX C12)
+			{PinNumber: "B3", NetName: "EC_I2C_PD_SCL_R", Enum: "I2C_PORT_PD", PinTypeOverride: "I2C_CLOCK"},
+			{PinNumber: "B2", NetName: "EC_I2C_PD_SDA_R", IO: "OD", PinTypeOverride: "I2C_DATA"},
+			{PinNumber: "F5", NetName: "EC_FAN_TACH", PinTypeOverride: "TACH"},
+			{PinNumber: "G1", NetName: "EC_KSI0", PinTypeOverride: "KEYBOARD"},
+			{PinNumber: "G2", NetName: "EC_KSO01", PinTypeOverride: "KEYBOARD"},
+		}
+
+		var buf bytes.Buffer
+		if err := GenerateDTSI(&buf, configs, chip); err != nil {
+			t.Fatalf("GenerateDTSI failed: %v", err)
+		}
+		out := buf.String()
+
+		expectedSnippets := []string{
+			"&i2c_ctrl0 {\n\tstatus = \"okay\";\n};",
+			"&i2c0_0 {",
+			"port = <NPCX_TACH_PORT_B>;",
+			"sample-clk = <NPCX_TACH_FREQ_LFCLK>;",
+			"pulses-per-round = <2>;",
+			"pinctrl-1 = <\n\t\t&ksi0_gpio_gp31\n\t\t&kso01_gpio_gp21\n\t>;",
+			"pinctrl-names = \"default\", \"sleep\";",
+		}
+		for _, snip := range expectedSnippets {
+			if !strings.Contains(out, snip) {
+				t.Errorf("NPCX output missing expected snippet:\n%s\nGot:\n%s", snip, out)
+			}
+		}
+	})
+
+	t.Run("ITE_KbdSleep_Tach", func(t *testing.T) {
+		chip := &Chip{
+			Name:   "ite-mock",
+			Vendor: "ite",
+			PinMap: map[string]string{
+				"D6": "TACH0A/GPD6",
+				"G1": "KSI0/GPG1",
+				"G2": "KSO0/GPG2",
+			},
+		}
+		configs := []PinConfig{
+			{PinNumber: "D6", NetName: "EC_FAN_TACH", PinTypeOverride: "TACH"},
+			{PinNumber: "G1", NetName: "EC_KSI0", PinTypeOverride: "KEYBOARD"},
+			{PinNumber: "G2", NetName: "EC_KSO0", PinTypeOverride: "KEYBOARD"},
+		}
+
+		var buf bytes.Buffer
+		if err := GenerateDTSI(&buf, configs, chip); err != nil {
+			t.Fatalf("GenerateDTSI failed: %v", err)
+		}
+		out := buf.String()
+
+		expectedSnippets := []string{
+			"channel = <IT8XXX2_TACH_CHANNEL_A>;",
+			"pulses-per-round = <2>;",
+			"pinctrl-1 = <\n\t\t&ksi0_sleep\n\t\t&kso0_sleep\n\t>;",
+			"pinctrl-names = \"default\", \"sleep\";",
+		}
+		for _, snip := range expectedSnippets {
+			if !strings.Contains(out, snip) {
+				t.Errorf("ITE output missing expected snippet:\n%s\nGot:\n%s", snip, out)
+			}
+		}
+	})
+}
+
+func TestGenerateDTSI_ITE_UART(t *testing.T) {
+	chip := &Chip{
+		Name:   "ite-mock",
+		Vendor: "ite",
+		PinMap: map[string]string{
+			"A3": "SOUT0/GPB1",
+			"A4": "SIN0/GPB0",
+			"N3": "SOUT1/GPH2/ID2",
+			"M3": "SIN1/GPH1/ID1",
+		},
+	}
+
+	t.Run("NonDefaultUART2Only", func(t *testing.T) {
+		configs := []PinConfig{
+			{PinNumber: "N3", NetName: "UART_EC_TX_DBG_RX", PinTypeOverride: "UART_TX"},
+			{PinNumber: "M3", NetName: "UART_DBG_TX_EC_RX", PinTypeOverride: "UART_RX"},
+		}
+		var buf bytes.Buffer
+		if err := GenerateDTSI(&buf, configs, chip); err != nil {
+			t.Fatalf("GenerateDTSI failed: %v", err)
+		}
+		out := buf.String()
+		for _, snip := range []string{
+			"&uart1 {\n\tstatus = \"disabled\";\n};",
+			"&ite_uart1_wrapper {\n\tstatus = \"disabled\";\n};",
+			"&uart2 {\n\tstatus = \"okay\";\n\tcurrent-speed = <115200>;\n};",
+			"&ite_uart2_wrapper {\n\tstatus = \"okay\";\n\tpinctrl-0 = <\n\t\t&uart2_rx_gph1_default\n\t\t&uart2_tx_gph2_default\n\t>;\n\tpinctrl-names = \"default\";\n};",
+		} {
+			if !strings.Contains(out, snip) {
+				t.Errorf("Expected non-default UART2 snippet:\n%s\nGot:\n%s", snip, out)
+			}
+		}
+	})
+
+	t.Run("DualUART_ConsoleOnUART1", func(t *testing.T) {
+		configs := []PinConfig{
+			{PinNumber: "A3", NetName: "UART_EC_TX_DBG_RX", PinTypeOverride: "UART_TX"},
+			{PinNumber: "N3", NetName: "EC_BASE_TX", PinTypeOverride: "UART_TX"},
+		}
+		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, "&uart1") || strings.Contains(out, "&uart2") {
+			t.Errorf("Expected no UART override when SOUT0 (uart1) is present, got:\n%s", out)
+		}
+	})
+}
diff --git a/zephyr_dtsi/parser.go b/zephyr_dtsi/parser.go
index 83b15a3..451f2dc 100644
--- a/zephyr_dtsi/parser.go
+++ b/zephyr_dtsi/parser.go
@@ -71,6 +71,30 @@
 	return -1
 }
 
+func findPinCol(header []string, pinCol string) int {
+	if pinCol != "" {
+		return findCol(header, pinCol)
+	}
+	for _, cand := range columnAliases["PinNumber"] {
+		normCand := normalize(cand)
+		for i, name := range header {
+			if strings.EqualFold(normalize(name), normCand) {
+				return i
+			}
+		}
+	}
+	for i, name := range header {
+		lower := strings.ToLower(name)
+		if strings.Contains(lower, "name") || strings.Contains(lower, "type") || strings.Contains(lower, "desc") {
+			continue
+		}
+		if strings.Contains(lower, "pin") || strings.Contains(lower, "bga") || strings.Contains(lower, "ball") {
+			return i
+		}
+	}
+	return -1
+}
+
 // findHeaderRow scans the first 10 rows for a row containing both Pin and Net columns.
 func findHeaderRow(records [][]string, pinCol string) ([]string, [][]string, error) {
 	if len(records) == 0 {
@@ -82,14 +106,13 @@
 		limit = len(records)
 	}
 
-	pinCandidates := append([]string{pinCol}, columnAliases["PinNumber"]...)
 	for i := 0; i < limit; i++ {
 		row := records[i]
 		if len(row) < 2 {
 			continue
 		}
 
-		pinIdx := findCol(row, pinCandidates...)
+		pinIdx := findPinCol(row, pinCol)
 		netIdx := findCol(row, columnAliases["NetName"]...)
 
 		if pinIdx >= 0 && netIdx >= 0 {
@@ -101,9 +124,12 @@
 	for i := 0; i < limit; i++ {
 		row := records[i]
 		netIdx := findCol(row, columnAliases["NetName"]...)
-		pinIdx := findCol(row, pinCandidates...)
+		pinIdx := findPinCol(row, pinCol)
 		if netIdx >= 0 && pinIdx == -1 {
-			return nil, nil, fmt.Errorf("missing required column for Pin Number (checked --pin-col=%q, 'PinNumber', 'Pin')", pinCol)
+			if pinCol != "" {
+				return nil, nil, fmt.Errorf("missing required column for Pin Number (checked --pin-col=%q)", pinCol)
+			}
+			return nil, nil, fmt.Errorf("missing required column for Pin Number (checked 'PinNumber', 'Pin', 'Ball', package headers)")
 		}
 		if pinIdx >= 0 && netIdx == -1 {
 			return nil, nil, fmt.Errorf("missing required column for Net Name (checked 'NetName', 'Signal Name')")
@@ -277,15 +303,11 @@
 }
 
 func parseRows(header []string, rows [][]string, pinCol string) ([]PinConfig, error) {
-	col := func(key string, extra ...string) int {
-		cands := columnAliases[key]
-		if len(extra) > 0 {
-			cands = append(extra, cands...)
-		}
-		return findCol(header, cands...)
+	col := func(key string) int {
+		return findCol(header, columnAliases[key]...)
 	}
 
-	pinIdx := col("PinNumber", pinCol)
+	pinIdx := findPinCol(header, pinCol)
 	netIdx := col("NetName")
 
 	if pinIdx < 0 || pinIdx >= len(header) {
@@ -335,12 +357,12 @@
 	var configs []PinConfig
 	for _, row := range rows {
 		netName := cell(row, netIdx)
-		if isIgnoredSignal(netName) {
+		rawIO := strings.ToUpper(cell(row, ioIdx))
+		sigType := strings.ToUpper(cell(row, typeIdx))
+		if sigType != "UNUSED_IN" && sigType != "UNUSED_OUT" && isIgnoredSignal(netName) {
 			continue
 		}
 
-		rawIO := strings.ToUpper(cell(row, ioIdx))
-		sigType := strings.ToUpper(cell(row, typeIdx))
 		rawDir := cell(row, dirIdx)
 		enumVal := strings.Trim(strings.ReplaceAll(cell(row, enumIdx), `""`, `"`), `"`)
 
diff --git a/zephyr_dtsi/parser_test.go b/zephyr_dtsi/parser_test.go
index 17c9889..27a605e 100644
--- a/zephyr_dtsi/parser_test.go
+++ b/zephyr_dtsi/parser_test.go
@@ -251,3 +251,34 @@
 		t.Fatalf("Expected fallback pin B3, got: %+v", configs)
 	}
 }
+
+func TestParseCSV_PackageHeaderAutoDetection(t *testing.T) {
+	csvData := `"128-pin VFBGA",IT82002 Pin Name,GPIO,Signal Name,Dir,Type
+M5,PWM0/GPA0,GPA0,EC_VOLDN_BTN_ODL,,UNUSED_IN
+M7,PWM6/SSCK/GPA6,GPA6,NC,,UNUSED_IN
+C1,SMDAT2ALT/GPF7,GPF7,NC,,
+L1,WRST#,WRST#,EC_ERST_L,IN,INPUT_L
+`
+	configs, err := ParseCSV(strings.NewReader(csvData), "")
+	if err != nil {
+		t.Fatalf("ParseCSV failed: %v", err)
+	}
+	if len(configs) != 3 {
+		t.Fatalf("Expected 3 configs (NC with UNUSED_IN kept, plain NC skipped), got %d: %+v", len(configs), configs)
+	}
+	if configs[0].PinNumber != "M5" || configs[0].PinName != "PWM0/GPA0" || configs[0].PinTypeOverride != "UNUSED_IN" {
+		t.Errorf("Unexpected config[0]: %+v", configs[0])
+	}
+	if configs[1].PinNumber != "M7" || configs[1].NetName != "NC" || configs[1].PinTypeOverride != "UNUSED_IN" {
+		t.Errorf("Unexpected config[1]: %+v", configs[1])
+	}
+	if configs[2].PinNumber != "L1" || configs[2].PinName != "WRST#" || configs[2].NetName != "EC_ERST_L" {
+		t.Errorf("Unexpected config[2]: %+v", configs[2])
+	}
+
+	// Ensure "IT82002 Pin Name" alone is not mistaken for a PinNumber column
+	noPinNumCSV := "IT82002 Pin Name,Signal Name\nPWM0/GPA0,EC_FOO\n"
+	if _, err := ParseCSV(strings.NewReader(noPinNumCSV), ""); err == nil || !strings.Contains(err.Error(), "missing required column for Pin Number") {
+		t.Errorf("Expected missing Pin Number column error when only Pin Name is present, got %v", err)
+	}
+}
diff --git a/zephyr_dtsi/test/generated.dtsi b/zephyr_dtsi/test/generated.dtsi
index 36d2893..7b9ae4e 100644
--- a/zephyr_dtsi/test/generated.dtsi
+++ b/zephyr_dtsi/test/generated.dtsi
@@ -66,6 +66,10 @@
 	pinctrl-names = "default";
 };
 
+&i2c_ctrl0 {
+	status = "okay";
+};
+
 &pwm0 {
 	status = "okay";
 	pinctrl-0 = <&pwm0_gpc3>;
@@ -82,7 +86,9 @@
 	status = "okay";
 	pinctrl-0 = <&ta1_1_in_gp40>;
 	pinctrl-names = "default";
-	port = <&gpioa>;
+	port = <NPCX_TACH_PORT_A>;
+	sample-clk = <NPCX_TACH_FREQ_LFCLK>;
+	pulses-per-round = <2>;
 };
 
 &kbd {
@@ -93,7 +99,13 @@
 		&kso00_gp21
 		&kso01_gp20
 	>;
-	pinctrl-names = "default";
+	pinctrl-1 = <
+		&ksi0_gpio_gp31
+		&ksi1_gpio_gp30
+		&kso00_gpio_gp21
+		&kso01_gpio_gp20
+	>;
+	pinctrl-names = "default", "sleep";
 	row-size = <2>;
 	col-size = <2>;
 };
diff --git a/zephyr_dtsi/test/generated_ite.dtsi b/zephyr_dtsi/test/generated_ite.dtsi
index ed0b9d4..845e774 100644
--- a/zephyr_dtsi/test/generated_ite.dtsi
+++ b/zephyr_dtsi/test/generated_ite.dtsi
@@ -88,6 +88,8 @@
 	status = "okay";
 	pinctrl-0 = <&tach0a_gpd6_default>;
 	pinctrl-names = "default";
+	channel = <IT8XXX2_TACH_CHANNEL_A>;
+	pulses-per-round = <2>;
 };
 
 &kbd {
@@ -98,7 +100,13 @@
 		&kso0_default
 		&kso1_default
 	>;
-	pinctrl-names = "default";
+	pinctrl-1 = <
+		&ksi0_sleep
+		&ksi1_sleep
+		&kso0_sleep
+		&kso1_sleep
+	>;
+	pinctrl-names = "default", "sleep";
 	row-size = <2>;
 	col-size = <2>;
 };
diff --git a/zephyr_dtsi/test/generated_npcx.dtsi b/zephyr_dtsi/test/generated_npcx.dtsi
index 36d2893..7b9ae4e 100644
--- a/zephyr_dtsi/test/generated_npcx.dtsi
+++ b/zephyr_dtsi/test/generated_npcx.dtsi
@@ -66,6 +66,10 @@
 	pinctrl-names = "default";
 };
 
+&i2c_ctrl0 {
+	status = "okay";
+};
+
 &pwm0 {
 	status = "okay";
 	pinctrl-0 = <&pwm0_gpc3>;
@@ -82,7 +86,9 @@
 	status = "okay";
 	pinctrl-0 = <&ta1_1_in_gp40>;
 	pinctrl-names = "default";
-	port = <&gpioa>;
+	port = <NPCX_TACH_PORT_A>;
+	sample-clk = <NPCX_TACH_FREQ_LFCLK>;
+	pulses-per-round = <2>;
 };
 
 &kbd {
@@ -93,7 +99,13 @@
 		&kso00_gp21
 		&kso01_gp20
 	>;
-	pinctrl-names = "default";
+	pinctrl-1 = <
+		&ksi0_gpio_gp31
+		&ksi1_gpio_gp30
+		&kso00_gpio_gp21
+		&kso01_gpio_gp20
+	>;
+	pinctrl-names = "default", "sleep";
 	row-size = <2>;
 	col-size = <2>;
 };