From 51f8d5eb335618acfea244ac8bc6bcf8e99ab439 Mon Sep 17 00:00:00 2001 From: Jae Gangemi Date: Sun, 19 Jul 2026 21:46:37 -0600 Subject: [PATCH] feat: expose MAC, chip revision, and chip features via eFuse reads Add Flasher.MAC()/ChipRevision()/ChipFeatures(), reading the factory-programmed base MAC, silicon revision, and feature list straight from eFuse registers, mirroring esptool's read_mac()/ get_major_chip_version()/get_minor_chip_version()/get_chip_features() per chip. - add ReadMAC/ReadChipRevision/ReadChipFeatures func-pointer fields to chipDef, with thin nil-checking dispatchers on Flasher - implement per-chip decoders for ESP32 (classic, lookup-table major revision), S2, S3 (including the ECO0 block-version workaround), C2, C3, C5, C6, H2, and P4-rev1; ESP8266 leaves all three nil - ReadRegister has no isStub() gate, so these work pre- and post-stub (no_reset/SkipStub included) - table-driven host tests per chip covering every decoder branch, plus dispatcher nil-chip/unsupported-chip cases --- pkg/espflasher/chip.go | 18 ++- pkg/espflasher/flasher.go | 55 +++++++ pkg/espflasher/flasher_test.go | 116 +++++++++++++++ pkg/espflasher/target_esp32.go | 158 +++++++++++++++++++++ pkg/espflasher/target_esp32_test.go | 139 ++++++++++++++++++ pkg/espflasher/target_esp32c2.go | 81 +++++++++++ pkg/espflasher/target_esp32c2_test.go | 96 +++++++++++++ pkg/espflasher/target_esp32c3.go | 93 +++++++++++- pkg/espflasher/target_esp32c3_test.go | 97 +++++++++++++ pkg/espflasher/target_esp32c5.go | 52 +++++++ pkg/espflasher/target_esp32c5_test.go | 71 +++++++++ pkg/espflasher/target_esp32c6.go | 67 +++++++++ pkg/espflasher/target_esp32c6_test.go | 87 ++++++++++++ pkg/espflasher/target_esp32h2.go | 57 +++++++- pkg/espflasher/target_esp32h2_test.go | 65 +++++++++ pkg/espflasher/target_esp32p4_rev1.go | 58 +++++++- pkg/espflasher/target_esp32p4_rev1_test.go | 65 +++++++++ pkg/espflasher/target_esp32s2.go | 99 +++++++++++++ pkg/espflasher/target_esp32s2_test.go | 107 ++++++++++++++ pkg/espflasher/target_esp32s3.go | 137 +++++++++++++++++- pkg/espflasher/target_esp32s3_test.go | 144 +++++++++++++++++++ 21 files changed, 1849 insertions(+), 13 deletions(-) create mode 100644 pkg/espflasher/target_esp32_test.go create mode 100644 pkg/espflasher/target_esp32c2_test.go diff --git a/pkg/espflasher/chip.go b/pkg/espflasher/chip.go index a622a35..f453d7d 100644 --- a/pkg/espflasher/chip.go +++ b/pkg/espflasher/chip.go @@ -1,6 +1,9 @@ package espflasher -import "fmt" +import ( + "fmt" + "net" +) // ChipType identifies the ESP chip family. type ChipType int @@ -135,6 +138,19 @@ type chipDef struct { // usesUSB/hardReset path. Nil for chips without a native-OTG reset // mechanism. HardResetOTG func(f *Flasher) bool + + // ReadMAC reads the factory-programmed base MAC address from eFuse. + // Nil for chips (ESP8266) that don't expose it via this scheme. + ReadMAC func(f *Flasher) (net.HardwareAddr, error) + + // ReadChipRevision reads the eFuse-encoded silicon revision. + // Nil for chips (ESP8266) that don't expose it via this scheme. + ReadChipRevision func(f *Flasher) (ChipRevision, error) + + // ReadChipFeatures reads (or, for chips with no runtime-detectable + // feature bits, returns a fixed list of) the chip's feature set. + // Nil for chips (ESP8266) that don't expose it via this scheme. + ReadChipFeatures func(f *Flasher) ([]string, error) } // chipDetectMagicRegAddr is the register address that has a different diff --git a/pkg/espflasher/flasher.go b/pkg/espflasher/flasher.go index a5beb15..2c52ae3 100644 --- a/pkg/espflasher/flasher.go +++ b/pkg/espflasher/flasher.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "fmt" "io" + "net" "runtime" "time" @@ -1347,6 +1348,60 @@ func (f *Flasher) FlashID() (uint8, uint16, error) { return mfgID, devID, nil } +// MAC returns the factory-programmed base MAC address read from eFuse. +// Works both pre- and post-stub, since ReadRegister uses the READ_REG +// command, which is implemented by both the ROM loader and the stub. +func (f *Flasher) MAC() (net.HardwareAddr, error) { + if f.chip == nil || f.chip.ReadMAC == nil { + return nil, &UnsupportedCommandError{Command: "read MAC (chip not detected or unsupported)"} + } + return f.chip.ReadMAC(f) +} + +// ChipRevision is the eFuse-encoded silicon revision. +type ChipRevision struct { + Major int + Minor int +} + +// String returns the revision formatted as "vMAJOR.MINOR". +func (r ChipRevision) String() string { + return fmt.Sprintf("v%d.%d", r.Major, r.Minor) +} + +// ChipRevision reads the eFuse-encoded silicon revision. Works both +// pre- and post-stub, like MAC. +func (f *Flasher) ChipRevision() (ChipRevision, error) { + if f.chip == nil || f.chip.ReadChipRevision == nil { + return ChipRevision{}, &UnsupportedCommandError{Command: "read chip revision (chip not detected or unsupported)"} + } + return f.chip.ReadChipRevision(f) +} + +// ChipFeatures returns a human-readable feature list, mirroring esptool's +// get_chip_features(). Works both pre- and post-stub, like MAC. +func (f *Flasher) ChipFeatures() ([]string, error) { + if f.chip == nil || f.chip.ReadChipFeatures == nil { + return nil, &UnsupportedCommandError{Command: "read chip features (chip not detected or unsupported)"} + } + return f.chip.ReadChipFeatures(f) +} + +// decodeEfuseMAC packs two adjacent 32-bit eFuse words into a 6-byte MAC +// address, mirroring esptool's read_mac(): struct.pack(">II", word1, word0) +// trimmed to the middle 6 bytes (the leading 2 bytes of word1 are CRC/other +// bits, not part of the MAC). +func decodeEfuseMAC(word0, word1 uint32) net.HardwareAddr { + return net.HardwareAddr{ + byte(word1 >> 8), + byte(word1), + byte(word0 >> 24), + byte(word0 >> 16), + byte(word0 >> 8), + byte(word0), + } +} + // runSPIFlashCommand executes a SPI flash command at the register level. // It configures the SPI peripheral to send 'cmd' as an 8-bit command, // optionally write 'data' bytes, and read back 'readBits' bits of response. diff --git a/pkg/espflasher/flasher_test.go b/pkg/espflasher/flasher_test.go index bf44dcd..6d2a917 100644 --- a/pkg/espflasher/flasher_test.go +++ b/pkg/espflasher/flasher_test.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "errors" "fmt" + "net" "testing" ) @@ -543,3 +544,118 @@ func TestGetSecurityInfo(t *testing.T) { } } } + +func TestChipRevisionString(t *testing.T) { + rev := ChipRevision{Major: 1, Minor: 2} + if got, want := rev.String(), "v1.2"; got != want { + t.Errorf("ChipRevision.String() = %q, want %q", got, want) + } +} + +func TestMACNilChip(t *testing.T) { + f := &Flasher{conn: &mockConnection{}} + _, err := f.MAC() + if err == nil { + t.Fatal("expected error for nil chip") + } + if _, ok := err.(*UnsupportedCommandError); !ok { + t.Errorf("expected UnsupportedCommandError, got %T: %v", err, err) + } +} + +func TestMACUnsupportedChip(t *testing.T) { + // ESP8266 leaves ReadMAC nil. + f := &Flasher{conn: &mockConnection{}, chip: chipDefs[ChipESP8266]} + _, err := f.MAC() + if err == nil { + t.Fatal("expected error for ESP8266") + } + if _, ok := err.(*UnsupportedCommandError); !ok { + t.Errorf("expected UnsupportedCommandError, got %T: %v", err, err) + } +} + +func TestChipRevisionNilChip(t *testing.T) { + f := &Flasher{conn: &mockConnection{}} + _, err := f.ChipRevision() + if err == nil { + t.Fatal("expected error for nil chip") + } + if _, ok := err.(*UnsupportedCommandError); !ok { + t.Errorf("expected UnsupportedCommandError, got %T: %v", err, err) + } +} + +func TestChipRevisionUnsupportedChip(t *testing.T) { + f := &Flasher{conn: &mockConnection{}, chip: chipDefs[ChipESP8266]} + _, err := f.ChipRevision() + if err == nil { + t.Fatal("expected error for ESP8266") + } + if _, ok := err.(*UnsupportedCommandError); !ok { + t.Errorf("expected UnsupportedCommandError, got %T: %v", err, err) + } +} + +func TestChipFeaturesNilChip(t *testing.T) { + f := &Flasher{conn: &mockConnection{}} + _, err := f.ChipFeatures() + if err == nil { + t.Fatal("expected error for nil chip") + } + if _, ok := err.(*UnsupportedCommandError); !ok { + t.Errorf("expected UnsupportedCommandError, got %T: %v", err, err) + } +} + +func TestChipFeaturesUnsupportedChip(t *testing.T) { + f := &Flasher{conn: &mockConnection{}, chip: chipDefs[ChipESP8266]} + _, err := f.ChipFeatures() + if err == nil { + t.Fatal("expected error for ESP8266") + } + if _, ok := err.(*UnsupportedCommandError); !ok { + t.Errorf("expected UnsupportedCommandError, got %T: %v", err, err) + } +} + +func TestMACDispatchesToChip(t *testing.T) { + f := &Flasher{conn: &mockConnection{}, chip: chipDefs[ChipESP32C3]} + mock := f.conn.(*mockConnection) + mock.readRegFunc = func(addr uint32) (uint32, error) { + switch addr { + case esp32c3EfuseBlock1Word0: + return 0x01020304, nil + case esp32c3EfuseBlock1Word0 + 4: + return 0x00000506, nil + } + return 0, nil + } + mac, err := f.MAC() + if err != nil { + t.Fatalf("MAC() failed: %v", err) + } + want := net.HardwareAddr{0x05, 0x06, 0x01, 0x02, 0x03, 0x04} + if mac.String() != want.String() { + t.Errorf("MAC() = %s, want %s", mac, want) + } +} + +// assertRegisterErrorsPropagate verifies that call returns a non-nil error +// when any single register in addrs fails to read, one at a time (all +// others succeed with 0). This proves every ReadRegister error-check branch +// in the decoder under test is reachable, not just the first. +func assertRegisterErrorsPropagate(t *testing.T, newFlasher func(readReg func(addr uint32) (uint32, error)) *Flasher, addrs []uint32, call func(f *Flasher) error) { + t.Helper() + for _, failAddr := range addrs { + f := newFlasher(func(addr uint32) (uint32, error) { + if addr == failAddr { + return 0, errors.New("register read failed") + } + return 0, nil + }) + if err := call(f); err == nil { + t.Errorf("expected error when register 0x%08X fails to read", failAddr) + } + } +} diff --git a/pkg/espflasher/target_esp32.go b/pkg/espflasher/target_esp32.go index 15f5712..10a4c34 100644 --- a/pkg/espflasher/target_esp32.go +++ b/pkg/espflasher/target_esp32.go @@ -1,5 +1,33 @@ package espflasher +import ( + "fmt" + "net" +) + +// ESP32 (classic) register addresses used for MAC/revision/feature +// decoding. Unlike every later chip, the classic ESP32's efuse layout has +// no BLOCK1; words are read directly off EFUSE_RD_REG_BASE, and the major +// chip revision isn't a bitfield — it's a 3-bit value assembled from two +// efuse bits plus one bit from an entirely separate SYSCON register, +// looked up in a table. +// Reference: esptool/targets/esp32.py (EFUSE_RD_REG_BASE, read_efuse(), +// APB_CTL_DATE_ADDR, get_major_chip_version/get_minor_chip_version/ +// get_pkg_version/get_chip_features). +const ( + esp32EfuseWord1 uint32 = 0x3FF5A004 // read_efuse(1) + esp32EfuseWord2 uint32 = 0x3FF5A008 // read_efuse(2) + esp32EfuseWord3 uint32 = 0x3FF5A00C // read_efuse(3) + esp32EfuseWord4 uint32 = 0x3FF5A010 // read_efuse(4) + esp32EfuseWord5 uint32 = 0x3FF5A014 // read_efuse(5) + esp32EfuseWord6 uint32 = 0x3FF5A018 // read_efuse(6) + + // APB_CTL_DATE_ADDR = DR_REG_SYSCON_BASE (0x3FF66000) + 0x7C. Bit 31 + // supplies the top bit of the 3-bit major-revision lookup index; it + // lives outside the eFuse block entirely. + esp32APBCtlDateReg uint32 = 0x3FF6607C +) + // ESP32 target definition. // Reference: https://github.com/espressif/esptool/blob/master/esptool/targets/esp32.py @@ -37,4 +65,134 @@ var defESP32 = &chipDef{ }, FlashSizes: defaultFlashSizes(), + + ReadMAC: esp32ReadMAC, + ReadChipRevision: esp32ReadChipRevision, + ReadChipFeatures: esp32ReadChipFeatures, +} + +// esp32ReadMAC reads the factory-programmed base MAC from eFuse. +// Reference: esptool/targets/esp32.py read_mac(). +func esp32ReadMAC(f *Flasher) (net.HardwareAddr, error) { + word1, err := f.ReadRegister(esp32EfuseWord1) + if err != nil { + return nil, err + } + word2, err := f.ReadRegister(esp32EfuseWord2) + if err != nil { + return nil, err + } + return decodeEfuseMAC(word1, word2), nil +} + +// esp32MajorChipVersionTable is esptool's lookup table mapping the 3-bit +// combined revision-bit value (from two eFuse bits plus one SYSCON bit) to +// the major chip revision. Combine values not present here (2, 4, 5, 6) +// map to major revision 0. +// Reference: esptool/targets/esp32.py get_major_chip_version(). +var esp32MajorChipVersionTable = map[uint32]int{ + 0: 0, + 1: 1, + 3: 2, + 7: 3, +} + +// esp32ReadChipRevision reads the eFuse-encoded silicon revision. Unlike +// every later chip, the major version isn't a bitfield: it's a lookup-table +// index assembled from two eFuse bits plus one bit read from a SYSCON +// register outside the eFuse block. +// Reference: esptool/targets/esp32.py get_major_chip_version()/ +// get_minor_chip_version(). +func esp32ReadChipRevision(f *Flasher) (ChipRevision, error) { + word3, err := f.ReadRegister(esp32EfuseWord3) + if err != nil { + return ChipRevision{}, err + } + word5, err := f.ReadRegister(esp32EfuseWord5) + if err != nil { + return ChipRevision{}, err + } + apbCtlDate, err := f.ReadRegister(esp32APBCtlDateReg) + if err != nil { + return ChipRevision{}, err + } + + revBit0 := (word3 >> 15) & 0x1 + revBit1 := (word5 >> 20) & 0x1 + revBit2 := (apbCtlDate >> 31) & 0x1 + combine := (revBit2 << 2) | (revBit1 << 1) | revBit0 + + major := esp32MajorChipVersionTable[combine] // default (unlisted) is 0 + minor := (word5 >> 24) & 0x3 + + return ChipRevision{Major: major, Minor: int(minor)}, nil +} + +// esp32CodingSchemeNames is esptool's literal mapping for the flash +// encoding-coding-scheme feature string. +// Reference: esptool/targets/esp32.py get_chip_features(). +var esp32CodingSchemeNames = map[uint32]string{ + 0: "None", + 1: "3/4", + 2: "Repeat (UNSUPPORTED)", + 3: "None (may contain encoding data)", +} + +// esp32ReadChipFeatures returns the chip feature list. +// Reference: esptool/targets/esp32.py get_chip_features(). +func esp32ReadChipFeatures(f *Flasher) ([]string, error) { + word3, err := f.ReadRegister(esp32EfuseWord3) + if err != nil { + return nil, err + } + word4, err := f.ReadRegister(esp32EfuseWord4) + if err != nil { + return nil, err + } + word6, err := f.ReadRegister(esp32EfuseWord6) + if err != nil { + return nil, err + } + + features := []string{"Wi-Fi"} + + if word3&(1<<1) == 0 { + features = append(features, "BT") + } + + if word3&(1<<0) != 0 { + features = append(features, "Single Core + LP Core") + } else { + features = append(features, "Dual Core + LP Core") + } + + if word3&(1<<13) != 0 { + if word3&(1<<12) != 0 { + features = append(features, "160MHz") + } else { + features = append(features, "240MHz") + } + } + + pkgVersion := ((word3 >> 9) & 0x7) | (((word3 >> 2) & 0x1) << 3) + switch pkgVersion { + case 2, 4, 5, 6: + features = append(features, "Embedded Flash") + } + if pkgVersion == 6 { + features = append(features, "Embedded PSRAM") + } + + if adcVref := (word4 >> 8) & 0x1F; adcVref != 0 { + features = append(features, "Vref calibration in eFuse") + } + + if word3>>14&0x1 != 0 { + features = append(features, "BLK3 partially reserved") + } + + codingScheme := word6 & 0x3 + features = append(features, fmt.Sprintf("Coding Scheme %s", esp32CodingSchemeNames[codingScheme])) + + return features, nil } diff --git a/pkg/espflasher/target_esp32_test.go b/pkg/espflasher/target_esp32_test.go new file mode 100644 index 0000000..2cc517a --- /dev/null +++ b/pkg/espflasher/target_esp32_test.go @@ -0,0 +1,139 @@ +package espflasher + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestESP32MAC(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + switch addr { + case esp32EfuseWord1: + return 0x60559ff7, nil + case esp32EfuseWord2: + return 0x00002ca2, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32} + + mac, err := f.MAC() + require.NoError(t, err) + assert.Equal(t, "2c:a2:60:55:9f:f7", mac.String()) +} + +// TestESP32ChipRevision covers every entry (and one unlisted gap value) of +// esptool's combine-value -> major-revision lookup table, plus the minor +// version bitfield. +func TestESP32ChipRevision(t *testing.T) { + tests := []struct { + name string + word3 uint32 + word5 uint32 + apbCtlDate uint32 + want ChipRevision + }{ + {"combine=0 -> major 0", 0, 0, 0, ChipRevision{0, 0}}, + {"combine=1 (bit0) -> major 1", 1 << 15, 0, 0, ChipRevision{1, 0}}, + {"combine=3 (bit0+bit1) -> major 2", 1 << 15, 1 << 20, 0, ChipRevision{2, 0}}, + {"combine=7 (all bits) -> major 3", 1 << 15, 1 << 20, 1 << 31, ChipRevision{3, 0}}, + {"combine=2 (bit1 only, unlisted) -> major 0", 0, 1 << 20, 0, ChipRevision{0, 0}}, + {"combine=4 (bit2 only, unlisted) -> major 0", 0, 0, 1 << 31, ChipRevision{0, 0}}, + {"minor=3", 0, 3 << 24, 0, ChipRevision{0, 3}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + switch addr { + case esp32EfuseWord3: + return tt.word3, nil + case esp32EfuseWord5: + return tt.word5, nil + case esp32APBCtlDateReg: + return tt.apbCtlDate, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32} + got, err := f.ChipRevision() + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestESP32ReadErrorsPropagate(t *testing.T) { + newF := func(readReg func(addr uint32) (uint32, error)) *Flasher { + return &Flasher{conn: &mockConnection{readRegFunc: readReg}, chip: defESP32} + } + assertRegisterErrorsPropagate(t, newF, []uint32{esp32EfuseWord1, esp32EfuseWord2}, + func(f *Flasher) error { _, err := f.MAC(); return err }) + assertRegisterErrorsPropagate(t, newF, []uint32{esp32EfuseWord3, esp32EfuseWord5, esp32APBCtlDateReg}, + func(f *Flasher) error { _, err := f.ChipRevision(); return err }) + assertRegisterErrorsPropagate(t, newF, []uint32{esp32EfuseWord3, esp32EfuseWord4, esp32EfuseWord6}, + func(f *Flasher) error { _, err := f.ChipFeatures(); return err }) +} + +func TestESP32ChipFeatures(t *testing.T) { + tests := []struct { + name string + word3 uint32 + word4 uint32 + word6 uint32 + want []string + }{ + { + "minimal: BT disabled, single core, no rated freq, pkg 0, no vref, no blk3, coding none", + (1 << 1) | (1 << 0), 0, 0, + []string{"Wi-Fi", "Single Core + LP Core", "Coding Scheme None"}, + }, + { + "BT enabled, dual core, 240MHz, embedded flash+psram (pkg6), vref, blk3, coding 3/4", + (6<<9 | 0<<2) | (1 << 13) | (1 << 14), // pkg_version=6 via bits[11:9]; freq rated; blk3 + 1 << 8, // adc_vref nonzero + 1, // coding scheme 1 + []string{"Wi-Fi", "BT", "Dual Core + LP Core", "240MHz", "Embedded Flash", "Embedded PSRAM", "Vref calibration in eFuse", "BLK3 partially reserved", "Coding Scheme 3/4"}, + }, + { + "160MHz, pkg 2 (flash only), coding 2 (repeat, unsupported)", + (2<<9 | 0<<2) | (1 << 13) | (1 << 12), + 0, + 2, + []string{"Wi-Fi", "BT", "Dual Core + LP Core", "160MHz", "Embedded Flash", "Coding Scheme Repeat (UNSUPPORTED)"}, + }, + { + "coding 3 (none, may contain encoding data)", + (1 << 1) | (1 << 0), // BT disabled (bit1), single core (bit0) + 0, + 3, + []string{"Wi-Fi", "Single Core + LP Core", "Coding Scheme None (may contain encoding data)"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + switch addr { + case esp32EfuseWord3: + return tt.word3, nil + case esp32EfuseWord4: + return tt.word4, nil + case esp32EfuseWord6: + return tt.word6, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32} + got, err := f.ChipFeatures() + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/espflasher/target_esp32c2.go b/pkg/espflasher/target_esp32c2.go index a0528cb..80ffe65 100644 --- a/pkg/espflasher/target_esp32c2.go +++ b/pkg/espflasher/target_esp32c2.go @@ -1,5 +1,21 @@ package espflasher +import ( + "fmt" + "net" +) + +// ESP32-C2 register addresses used for MAC/revision/feature decoding. +// Reference: esptool/targets/esp32c2.py +// +// Unlike C3/C5/C6/H2, the C2's MAC/revision eFuse words live in BLOCK2 +// (offset +0x040 from EFUSE_BASE), not BLOCK1 (+0x044). +const ( + esp32c2EfuseBlock2Word0 uint32 = 0x60008840 // MAC_EFUSE_REG (num_word 0) + esp32c2EfuseBlock2Word1 uint32 = 0x60008844 // num_word 1 + esp32c2EfuseBlock2Word7 uint32 = 0x6000885C // num_word 7 +) + // ESP32-C2 (ESP8684) target definition. // Reference: https://github.com/espressif/esptool/blob/master/esptool/targets/esp32c2.py @@ -40,4 +56,69 @@ var defESP32C2 = &chipDef{ }, FlashSizes: defaultFlashSizes(), + + ReadMAC: esp32c2ReadMAC, + ReadChipRevision: esp32c2ReadChipRevision, + ReadChipFeatures: esp32c2ReadChipFeatures, +} + +// esp32c2ReadMAC reads the factory-programmed base MAC from eFuse. +// Reference: esptool/targets/esp32c2.py read_mac(). +func esp32c2ReadMAC(f *Flasher) (net.HardwareAddr, error) { + word0, err := f.ReadRegister(esp32c2EfuseBlock2Word0) + if err != nil { + return nil, err + } + word1, err := f.ReadRegister(esp32c2EfuseBlock2Word0 + 4) + if err != nil { + return nil, err + } + return decodeEfuseMAC(word0, word1), nil +} + +// esp32c2ReadChipRevision reads the eFuse-encoded silicon revision. +// Reference: esptool/targets/esp32c2.py get_major_chip_version()/ +// get_minor_chip_version(). +func esp32c2ReadChipRevision(f *Flasher) (ChipRevision, error) { + word1, err := f.ReadRegister(esp32c2EfuseBlock2Word1) + if err != nil { + return ChipRevision{}, err + } + major := (word1 >> 20) & 0x3 + minor := (word1 >> 16) & 0xF + return ChipRevision{Major: int(major), Minor: int(minor)}, nil +} + +// esp32c2ReadChipFeatures returns the chip feature list. +// Reference: esptool/targets/esp32c2.py get_chip_features(). +func esp32c2ReadChipFeatures(f *Flasher) ([]string, error) { + word7, err := f.ReadRegister(esp32c2EfuseBlock2Word7) + if err != nil { + return nil, err + } + + features := []string{"Wi-Fi", "BT 5 (LE)", "Single Core", "120MHz"} + + flashCap := (word7 >> 29) & 0x7 + flash, ok := map[uint32]string{ + 1: "Embedded Flash 4MB", + 2: "Embedded Flash 2MB", + 3: "Embedded Flash 1MB", + }[flashCap] + if !ok && flashCap != 0 { + flash = "Unknown Embedded Flash" + } + if flash != "" { + vendorID := (word7 >> 24) & 0x7 + vendor := map[uint32]string{ + 1: "XMC", + 2: "GD", + 3: "FM", + 4: "TT", + 5: "ZBIT", + }[vendorID] + features = append(features, fmt.Sprintf("%s (%s)", flash, vendor)) + } + + return features, nil } diff --git a/pkg/espflasher/target_esp32c2_test.go b/pkg/espflasher/target_esp32c2_test.go new file mode 100644 index 0000000..9d3eee2 --- /dev/null +++ b/pkg/espflasher/target_esp32c2_test.go @@ -0,0 +1,96 @@ +package espflasher + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestESP32C2MAC(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + switch addr { + case esp32c2EfuseBlock2Word0: + return 0x60559ff7, nil + case esp32c2EfuseBlock2Word0 + 4: + return 0x00002ca2, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32C2} + + mac, err := f.MAC() + require.NoError(t, err) + assert.Equal(t, "2c:a2:60:55:9f:f7", mac.String()) +} + +func TestESP32C2ChipRevision(t *testing.T) { + tests := []struct { + name string + word1 uint32 + want ChipRevision + }{ + {"v0.0", 0x00000000, ChipRevision{0, 0}}, + {"v1.1", (1 << 20) | (1 << 16), ChipRevision{1, 1}}, + {"max", (3 << 20) | (0xF << 16), ChipRevision{3, 15}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + if addr == esp32c2EfuseBlock2Word1 { + return tt.word1, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32C2} + got, err := f.ChipRevision() + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestESP32C2ReadErrorsPropagate(t *testing.T) { + newF := func(readReg func(addr uint32) (uint32, error)) *Flasher { + return &Flasher{conn: &mockConnection{readRegFunc: readReg}, chip: defESP32C2} + } + assertRegisterErrorsPropagate(t, newF, []uint32{esp32c2EfuseBlock2Word0, esp32c2EfuseBlock2Word0 + 4}, + func(f *Flasher) error { _, err := f.MAC(); return err }) + assertRegisterErrorsPropagate(t, newF, []uint32{esp32c2EfuseBlock2Word1}, + func(f *Flasher) error { _, err := f.ChipRevision(); return err }) + assertRegisterErrorsPropagate(t, newF, []uint32{esp32c2EfuseBlock2Word7}, + func(f *Flasher) error { _, err := f.ChipFeatures(); return err }) +} + +func TestESP32C2ChipFeatures(t *testing.T) { + tests := []struct { + name string + word7 uint32 + want []string + }{ + {"no flash", 0, []string{"Wi-Fi", "BT 5 (LE)", "Single Core", "120MHz"}}, + {"4MB XMC", (1 << 29) | (1 << 24), []string{"Wi-Fi", "BT 5 (LE)", "Single Core", "120MHz", "Embedded Flash 4MB (XMC)"}}, + {"1MB unknown vendor", (3 << 29), []string{"Wi-Fi", "BT 5 (LE)", "Single Core", "120MHz", "Embedded Flash 1MB ()"}}, + {"unknown cap", (7 << 29), []string{"Wi-Fi", "BT 5 (LE)", "Single Core", "120MHz", "Unknown Embedded Flash ()"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + if addr == esp32c2EfuseBlock2Word7 { + return tt.word7, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32C2} + got, err := f.ChipFeatures() + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/espflasher/target_esp32c3.go b/pkg/espflasher/target_esp32c3.go index 156c273..518b12c 100644 --- a/pkg/espflasher/target_esp32c3.go +++ b/pkg/espflasher/target_esp32c3.go @@ -1,6 +1,9 @@ package espflasher -import "fmt" +import ( + "fmt" + "net" +) // ESP32-C3 register addresses for USB interface detection and watchdog control. // Reference: esptool/targets/esp32c3.py @@ -9,8 +12,8 @@ const ( // Revision < 1 (ECO0-ECO3): base 0x3FCDF064 // Revision >= 1 (ECO4+): base 0x3FCDF060 // The actual register is 24 bytes (+0x18) from the base address. - esp32c3UARTDevBufNoRev0 uint32 = 0x3FCDF07C // 0x3FCDF064 + 24 - esp32c3UARTDevBufNoRev101 uint32 = 0x3FCDF078 // 0x3FCDF060 + 24 + esp32c3UARTDevBufNoRev0 uint32 = 0x3FCDF07C // 0x3FCDF064 + 24 + esp32c3UARTDevBufNoRev101 uint32 = 0x3FCDF078 // 0x3FCDF060 + 24 esp32c3UARTDevBufNoUSBJTAGSerial uint32 = 3 // USB-JTAG/Serial active // Efuse register for chip version detection. @@ -27,6 +30,14 @@ const ( esp32c3RTCCntlSWDAutoFeedEn uint32 = 1 << 31 esp32c3RTCCntlSWDWProtect uint32 = 0x600080B0 esp32c3RTCCntlSWDWKey uint32 = 0x8F1D312A + + // EFUSE_BLOCK1 base and words used for MAC/revision/feature decoding. + // Reference: esptool/targets/esp32c3.py (EFUSE_BLOCK1_ADDR, MAC_EFUSE_REG, + // get_major_chip_version/get_minor_chip_version/get_flash_cap/get_flash_vendor). + esp32c3EfuseBlock1Word0 uint32 = 0x60008844 // MAC_EFUSE_REG (num_word 0) + esp32c3EfuseBlock1Word3 uint32 = 0x60008850 // num_word 3 + esp32c3EfuseBlock1Word4 uint32 = 0x60008854 // num_word 4 + esp32c3EfuseBlock1Word5 uint32 = 0x60008858 // num_word 5 ) // ESP32-C3 target definition. @@ -70,6 +81,10 @@ var defESP32C3 = &chipDef{ FlashSizes: defaultFlashSizes(), PostConnect: esp32c3PostConnect, + + ReadMAC: esp32c3ReadMAC, + ReadChipRevision: esp32c3ReadChipRevision, + ReadChipFeatures: esp32c3ReadChipFeatures, } // esp32c3ChipRevision reads the chip revision from efuse. @@ -158,3 +173,75 @@ func disableWatchdogsESP32C3(f *Flasher) error { return nil } + +// esp32c3ReadMAC reads the factory-programmed base MAC from eFuse. +// Reference: esptool/targets/esp32c3.py read_mac(). +func esp32c3ReadMAC(f *Flasher) (net.HardwareAddr, error) { + word0, err := f.ReadRegister(esp32c3EfuseBlock1Word0) + if err != nil { + return nil, err + } + word1, err := f.ReadRegister(esp32c3EfuseBlock1Word0 + 4) + if err != nil { + return nil, err + } + return decodeEfuseMAC(word0, word1), nil +} + +// esp32c3ReadChipRevision reads the eFuse-encoded silicon revision. +// Reference: esptool/targets/esp32c3.py get_major_chip_version()/ +// get_minor_chip_version(). +func esp32c3ReadChipRevision(f *Flasher) (ChipRevision, error) { + word3, err := f.ReadRegister(esp32c3EfuseBlock1Word3) + if err != nil { + return ChipRevision{}, err + } + word5, err := f.ReadRegister(esp32c3EfuseBlock1Word5) + if err != nil { + return ChipRevision{}, err + } + major := (word5 >> 24) & 0x3 + hi := (word5 >> 23) & 0x1 + low := (word3 >> 18) & 0x7 + minor := (hi << 3) + low + return ChipRevision{Major: int(major), Minor: int(minor)}, nil +} + +// esp32c3ReadChipFeatures returns the chip feature list. +// Reference: esptool/targets/esp32c3.py get_chip_features(). +func esp32c3ReadChipFeatures(f *Flasher) ([]string, error) { + word3, err := f.ReadRegister(esp32c3EfuseBlock1Word3) + if err != nil { + return nil, err + } + word4, err := f.ReadRegister(esp32c3EfuseBlock1Word4) + if err != nil { + return nil, err + } + + features := []string{"Wi-Fi", "BT 5 (LE)", "Single Core", "160MHz"} + + flashCap := (word3 >> 27) & 0x7 + flash, ok := map[uint32]string{ + 1: "Embedded Flash 4MB", + 2: "Embedded Flash 2MB", + 3: "Embedded Flash 1MB", + 4: "Embedded Flash 8MB", + }[flashCap] + if !ok && flashCap != 0 { + flash = "Unknown Embedded Flash" + } + if flash != "" { + vendorID := word4 & 0x7 + vendor := map[uint32]string{ + 1: "XMC", + 2: "GD", + 3: "FM", + 4: "TT", + 5: "ZBIT", + }[vendorID] + features = append(features, fmt.Sprintf("%s (%s)", flash, vendor)) + } + + return features, nil +} diff --git a/pkg/espflasher/target_esp32c3_test.go b/pkg/espflasher/target_esp32c3_test.go index 2cfede4..45dea03 100644 --- a/pkg/espflasher/target_esp32c3_test.go +++ b/pkg/espflasher/target_esp32c3_test.go @@ -190,6 +190,103 @@ func TestESP32C3PostConnectUART(t *testing.T) { assert.False(t, f.usesUSB, "usesUSB should be false for UART") } +func TestESP32C3MAC(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + switch addr { + case esp32c3EfuseBlock1Word0: + return 0x60559ff7, nil + case esp32c3EfuseBlock1Word0 + 4: + return 0x00002ca2, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32C3} + + mac, err := f.MAC() + require.NoError(t, err) + assert.Equal(t, "2c:a2:60:55:9f:f7", mac.String()) +} + +func TestESP32C3ChipRevision(t *testing.T) { + tests := []struct { + name string + word3 uint32 + word5 uint32 + want ChipRevision + }{ + {"v0.0", 0x00000000, 0x00000000, ChipRevision{0, 0}}, + {"v1.0", 0x00000000, 0x01000000, ChipRevision{1, 0}}, + {"v1.1", 0x00040000, 0x01000000, ChipRevision{1, 1}}, + {"max", 0x001c0000, 0x03800000, ChipRevision{3, 15}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + switch addr { + case esp32c3EfuseBlock1Word3: + return tt.word3, nil + case esp32c3EfuseBlock1Word5: + return tt.word5, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32C3} + got, err := f.ChipRevision() + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestESP32C3ChipFeatures(t *testing.T) { + tests := []struct { + name string + word3 uint32 + word4 uint32 + want []string + }{ + {"no flash", 0, 0, []string{"Wi-Fi", "BT 5 (LE)", "Single Core", "160MHz"}}, + {"4MB XMC", 1 << 27, 1, []string{"Wi-Fi", "BT 5 (LE)", "Single Core", "160MHz", "Embedded Flash 4MB (XMC)"}}, + {"8MB unknown vendor", 4 << 27, 0, []string{"Wi-Fi", "BT 5 (LE)", "Single Core", "160MHz", "Embedded Flash 8MB ()"}}, + {"unknown cap", 6 << 27, 0, []string{"Wi-Fi", "BT 5 (LE)", "Single Core", "160MHz", "Unknown Embedded Flash ()"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + switch addr { + case esp32c3EfuseBlock1Word3: + return tt.word3, nil + case esp32c3EfuseBlock1Word4: + return tt.word4, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32C3} + got, err := f.ChipFeatures() + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestESP32C3ReadErrorsPropagate(t *testing.T) { + newF := func(readReg func(addr uint32) (uint32, error)) *Flasher { + return &Flasher{conn: &mockConnection{readRegFunc: readReg}, chip: defESP32C3} + } + assertRegisterErrorsPropagate(t, newF, []uint32{esp32c3EfuseBlock1Word0, esp32c3EfuseBlock1Word0 + 4}, + func(f *Flasher) error { _, err := f.MAC(); return err }) + assertRegisterErrorsPropagate(t, newF, []uint32{esp32c3EfuseBlock1Word3, esp32c3EfuseBlock1Word5}, + func(f *Flasher) error { _, err := f.ChipRevision(); return err }) + assertRegisterErrorsPropagate(t, newF, []uint32{esp32c3EfuseBlock1Word3, esp32c3EfuseBlock1Word4}, + func(f *Flasher) error { _, err := f.ChipFeatures(); return err }) +} + func TestESP32C3PostConnectSecureMode(t *testing.T) { // Simulate read error (secure download mode) mc := &mockConnection{ diff --git a/pkg/espflasher/target_esp32c5.go b/pkg/espflasher/target_esp32c5.go index c33dd22..e14b5cf 100644 --- a/pkg/espflasher/target_esp32c5.go +++ b/pkg/espflasher/target_esp32c5.go @@ -1,5 +1,7 @@ package espflasher +import "net" + // ESP32-C5 register addresses for USB interface detection and watchdog control. // Reference: esptool/targets/esp32c5.py const ( @@ -10,6 +12,12 @@ const ( esp32c5LPWDTWProtect uint32 = 0x600B1C18 esp32c5LPWDTSWDConf uint32 = 0x600B1C1C esp32c5LPWDTSWDWProtect uint32 = 0x600B1C20 + + // EFUSE_BLOCK1 words used for MAC/revision decoding. + // Reference: esptool/targets/esp32c5.py (EFUSE_BLOCK1_ADDR, MAC_EFUSE_REG, + // get_major_chip_version/get_minor_chip_version). + esp32c5EfuseBlock1Word0 uint32 = 0x600B4844 // MAC_EFUSE_REG (num_word 0) + esp32c5EfuseBlock1Word2 uint32 = 0x600B484C // num_word 2 ) // ESP32-C5 target definition. @@ -53,6 +61,10 @@ var defESP32C5 = &chipDef{ FlashSizes: defaultFlashSizes(), PostConnect: esp32c5PostConnect, + + ReadMAC: esp32c5ReadMAC, + ReadChipRevision: esp32c5ReadChipRevision, + ReadChipFeatures: esp32c5ReadChipFeatures, } // esp32c5PostConnect detects the USB interface type and disables watchdogs @@ -75,3 +87,43 @@ func esp32c5PostConnect(f *Flasher) error { return nil } + +// esp32c5ReadMAC reads the factory-programmed base MAC from eFuse. +// Reference: esptool/targets/esp32c5.py read_mac(). +func esp32c5ReadMAC(f *Flasher) (net.HardwareAddr, error) { + word0, err := f.ReadRegister(esp32c5EfuseBlock1Word0) + if err != nil { + return nil, err + } + word1, err := f.ReadRegister(esp32c5EfuseBlock1Word0 + 4) + if err != nil { + return nil, err + } + return decodeEfuseMAC(word0, word1), nil +} + +// esp32c5ReadChipRevision reads the eFuse-encoded silicon revision. +// Reference: esptool/targets/esp32c5.py get_major_chip_version()/ +// get_minor_chip_version(). +func esp32c5ReadChipRevision(f *Flasher) (ChipRevision, error) { + word2, err := f.ReadRegister(esp32c5EfuseBlock1Word2) + if err != nil { + return ChipRevision{}, err + } + major := (word2 >> 4) & 0x3 + minor := word2 & 0xF + return ChipRevision{Major: int(major), Minor: int(minor)}, nil +} + +// esp32c5ReadChipFeatures returns the chip feature list. ESP32-C5 has no +// runtime-detectable flash/PSRAM eFuse bits in esptool, so this is a fixed +// list. Reference: esptool/targets/esp32c5.py get_chip_features(). +func esp32c5ReadChipFeatures(f *Flasher) ([]string, error) { + return []string{ + "Wi-Fi 6 (dual-band)", + "BT 5 (LE)", + "IEEE802.15.4", + "Single Core + LP Core", + "240MHz", + }, nil +} diff --git a/pkg/espflasher/target_esp32c5_test.go b/pkg/espflasher/target_esp32c5_test.go index 723da25..91fd4a8 100644 --- a/pkg/espflasher/target_esp32c5_test.go +++ b/pkg/espflasher/target_esp32c5_test.go @@ -53,6 +53,77 @@ func TestESP32C5PostConnectUART(t *testing.T) { assert.False(t, f.usesUSB, "usesUSB should be false for UART") } +func TestESP32C5MAC(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + switch addr { + case esp32c5EfuseBlock1Word0: + return 0x60559ff7, nil + case esp32c5EfuseBlock1Word0 + 4: + return 0x00002ca2, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32C5} + + mac, err := f.MAC() + require.NoError(t, err) + assert.Equal(t, "2c:a2:60:55:9f:f7", mac.String()) +} + +func TestESP32C5ChipRevision(t *testing.T) { + tests := []struct { + name string + word2 uint32 + want ChipRevision + }{ + {"v0.0", 0x00000000, ChipRevision{0, 0}}, + {"v1.1", (1 << 4) | 1, ChipRevision{1, 1}}, + {"max", (3 << 4) | 0xF, ChipRevision{3, 15}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + if addr == esp32c5EfuseBlock1Word2 { + return tt.word2, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32C5} + got, err := f.ChipRevision() + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestESP32C5ChipFeatures(t *testing.T) { + mc := &mockConnection{} + f := &Flasher{conn: mc, chip: defESP32C5} + got, err := f.ChipFeatures() + require.NoError(t, err) + assert.Equal(t, []string{ + "Wi-Fi 6 (dual-band)", + "BT 5 (LE)", + "IEEE802.15.4", + "Single Core + LP Core", + "240MHz", + }, got) +} + +func TestESP32C5ReadErrorsPropagate(t *testing.T) { + newF := func(readReg func(addr uint32) (uint32, error)) *Flasher { + return &Flasher{conn: &mockConnection{readRegFunc: readReg}, chip: defESP32C5} + } + assertRegisterErrorsPropagate(t, newF, []uint32{esp32c5EfuseBlock1Word0, esp32c5EfuseBlock1Word0 + 4}, + func(f *Flasher) error { _, err := f.MAC(); return err }) + assertRegisterErrorsPropagate(t, newF, []uint32{esp32c5EfuseBlock1Word2}, + func(f *Flasher) error { _, err := f.ChipRevision(); return err }) +} + func TestESP32C5PostConnectSecureMode(t *testing.T) { // Simulate read error (secure download mode) mc := &mockConnection{ diff --git a/pkg/espflasher/target_esp32c6.go b/pkg/espflasher/target_esp32c6.go index ecd73a7..edde3c0 100644 --- a/pkg/espflasher/target_esp32c6.go +++ b/pkg/espflasher/target_esp32c6.go @@ -1,5 +1,7 @@ package espflasher +import "net" + // ESP32-C6 register addresses for USB interface detection and watchdog control. // Reference: esptool/targets/esp32c6.py const ( @@ -10,6 +12,13 @@ const ( esp32c6LPWDTWProtect uint32 = 0x600B1C18 esp32c6LPWDTSWDConf uint32 = 0x600B1C1C esp32c6LPWDTSWDWProtect uint32 = 0x600B1C20 + + // EFUSE_BLOCK1 words used for MAC/revision/feature decoding. + // Reference: esptool/targets/esp32c6.py (EFUSE_BLOCK1_ADDR, MAC_EFUSE_REG, + // get_major_chip_version/get_minor_chip_version/get_flash_cap). + esp32c6EfuseBlock1Word0 uint32 = 0x600B0844 // MAC_EFUSE_REG (num_word 0) + esp32c6EfuseBlock1Word3 uint32 = 0x600B0850 // num_word 3 + esp32c6EfuseBlock1Word4 uint32 = 0x600B0854 // num_word 4 ) // ESP32-C6 target definition. @@ -53,6 +62,10 @@ var defESP32C6 = &chipDef{ FlashSizes: defaultFlashSizes(), PostConnect: esp32c6PostConnect, + + ReadMAC: esp32c6ReadMAC, + ReadChipRevision: esp32c6ReadChipRevision, + ReadChipFeatures: esp32c6ReadChipFeatures, } // esp32c6PostConnect detects the USB interface type and disables watchdogs @@ -75,3 +88,57 @@ func esp32c6PostConnect(f *Flasher) error { return nil } + +// esp32c6ReadMAC reads the factory-programmed base MAC from eFuse. +// Reference: esptool/targets/esp32c6.py read_mac(). +func esp32c6ReadMAC(f *Flasher) (net.HardwareAddr, error) { + word0, err := f.ReadRegister(esp32c6EfuseBlock1Word0) + if err != nil { + return nil, err + } + word1, err := f.ReadRegister(esp32c6EfuseBlock1Word0 + 4) + if err != nil { + return nil, err + } + return decodeEfuseMAC(word0, word1), nil +} + +// esp32c6ReadChipRevision reads the eFuse-encoded silicon revision. +// Reference: esptool/targets/esp32c6.py get_major_chip_version()/ +// get_minor_chip_version(). +func esp32c6ReadChipRevision(f *Flasher) (ChipRevision, error) { + word3, err := f.ReadRegister(esp32c6EfuseBlock1Word3) + if err != nil { + return ChipRevision{}, err + } + major := (word3 >> 22) & 0x3 + minor := (word3 >> 18) & 0xF + return ChipRevision{Major: int(major), Minor: int(minor)}, nil +} + +// esp32c6ReadChipFeatures returns the chip feature list. +// Reference: esptool/targets/esp32c6.py get_chip_features(). +func esp32c6ReadChipFeatures(f *Flasher) ([]string, error) { + word4, err := f.ReadRegister(esp32c6EfuseBlock1Word4) + if err != nil { + return nil, err + } + + flashCap := word4 & 0x7 + flash, ok := map[uint32]string{ + 1: "Embedded Flash 4MB", + 2: "Embedded Flash 8MB", + }[flashCap] + if !ok { + flash = "Unknown Embedded Flash" + } + + return []string{ + "Wi-Fi 6", + "BT 5 (LE)", + "IEEE802.15.4", + "Single Core + LP Core", + "160MHz", + flash, + }, nil +} diff --git a/pkg/espflasher/target_esp32c6_test.go b/pkg/espflasher/target_esp32c6_test.go index e863790..2e7aaf4 100644 --- a/pkg/espflasher/target_esp32c6_test.go +++ b/pkg/espflasher/target_esp32c6_test.go @@ -53,6 +53,93 @@ func TestESP32C6PostConnectUART(t *testing.T) { assert.False(t, f.usesUSB, "usesUSB should be false for UART") } +func TestESP32C6MAC(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + switch addr { + case esp32c6EfuseBlock1Word0: + return 0x60559ff7, nil + case esp32c6EfuseBlock1Word0 + 4: + return 0x00002ca2, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32C6} + + mac, err := f.MAC() + require.NoError(t, err) + assert.Equal(t, "2c:a2:60:55:9f:f7", mac.String()) +} + +func TestESP32C6ChipRevision(t *testing.T) { + tests := []struct { + name string + word3 uint32 + want ChipRevision + }{ + {"v0.0", 0x00000000, ChipRevision{0, 0}}, + {"v1.1", (1 << 22) | (1 << 18), ChipRevision{1, 1}}, + {"max", (3 << 22) | (0xF << 18), ChipRevision{3, 15}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + if addr == esp32c6EfuseBlock1Word3 { + return tt.word3, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32C6} + got, err := f.ChipRevision() + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestESP32C6ChipFeatures(t *testing.T) { + tests := []struct { + name string + word4 uint32 + want []string + }{ + {"4MB", 1, []string{"Wi-Fi 6", "BT 5 (LE)", "IEEE802.15.4", "Single Core + LP Core", "160MHz", "Embedded Flash 4MB"}}, + {"8MB", 2, []string{"Wi-Fi 6", "BT 5 (LE)", "IEEE802.15.4", "Single Core + LP Core", "160MHz", "Embedded Flash 8MB"}}, + {"unknown", 0, []string{"Wi-Fi 6", "BT 5 (LE)", "IEEE802.15.4", "Single Core + LP Core", "160MHz", "Unknown Embedded Flash"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + if addr == esp32c6EfuseBlock1Word4 { + return tt.word4, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32C6} + got, err := f.ChipFeatures() + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestESP32C6ReadErrorsPropagate(t *testing.T) { + newF := func(readReg func(addr uint32) (uint32, error)) *Flasher { + return &Flasher{conn: &mockConnection{readRegFunc: readReg}, chip: defESP32C6} + } + assertRegisterErrorsPropagate(t, newF, []uint32{esp32c6EfuseBlock1Word0, esp32c6EfuseBlock1Word0 + 4}, + func(f *Flasher) error { _, err := f.MAC(); return err }) + assertRegisterErrorsPropagate(t, newF, []uint32{esp32c6EfuseBlock1Word3}, + func(f *Flasher) error { _, err := f.ChipRevision(); return err }) + assertRegisterErrorsPropagate(t, newF, []uint32{esp32c6EfuseBlock1Word4}, + func(f *Flasher) error { _, err := f.ChipFeatures(); return err }) +} + func TestESP32C6PostConnectSecureMode(t *testing.T) { // Simulate read error (secure download mode) mc := &mockConnection{ diff --git a/pkg/espflasher/target_esp32h2.go b/pkg/espflasher/target_esp32h2.go index aaf40b9..1f9cc37 100644 --- a/pkg/espflasher/target_esp32h2.go +++ b/pkg/espflasher/target_esp32h2.go @@ -1,15 +1,25 @@ package espflasher +import "net" + // ESP32-H2 register addresses for USB interface detection and watchdog control. // Reference: esptool/targets/esp32h2.py const ( esp32h2UARTDevBufNo uint32 = 0x4084FEFC // ROM .bss: active console interface esp32h2UARTDevBufNoUSBJTAGSerial uint32 = 3 // USB-JTAG/Serial active - esp32h2LPWDTConfig0 uint32 = 0x600B1C00 - esp32h2LPWDTWProtect uint32 = 0x600B1C1C // H2 offset differs from C6 - esp32h2LPWDTSWDConf uint32 = 0x600B1C20 - esp32h2LPWDTSWDWProtect uint32 = 0x600B1C24 + esp32h2LPWDTConfig0 uint32 = 0x600B1C00 + esp32h2LPWDTWProtect uint32 = 0x600B1C1C // H2 offset differs from C6 + esp32h2LPWDTSWDConf uint32 = 0x600B1C20 + esp32h2LPWDTSWDWProtect uint32 = 0x600B1C24 + + // EFUSE_BLOCK1 words used for MAC/revision decoding. H2 shares C6's + // EFUSE_BASE (0x600B0800) but has a 3-bit minor-version mask, not C6's + // 4-bit mask. + // Reference: esptool/targets/esp32h2.py (EFUSE_BLOCK1_ADDR, MAC_EFUSE_REG, + // get_major_chip_version/get_minor_chip_version). + esp32h2EfuseBlock1Word0 uint32 = 0x600B0844 // MAC_EFUSE_REG (num_word 0) + esp32h2EfuseBlock1Word3 uint32 = 0x600B0850 // num_word 3 ) // ESP32-H2 target definition. @@ -54,6 +64,10 @@ var defESP32H2 = &chipDef{ FlashSizes: defaultFlashSizes(), PostConnect: esp32h2PostConnect, + + ReadMAC: esp32h2ReadMAC, + ReadChipRevision: esp32h2ReadChipRevision, + ReadChipFeatures: esp32h2ReadChipFeatures, } // esp32h2PostConnect detects the USB interface type and disables watchdogs @@ -76,3 +90,38 @@ func esp32h2PostConnect(f *Flasher) error { return nil } + +// esp32h2ReadMAC reads the factory-programmed base MAC from eFuse. +// Reference: esptool/targets/esp32h2.py read_mac(). +func esp32h2ReadMAC(f *Flasher) (net.HardwareAddr, error) { + word0, err := f.ReadRegister(esp32h2EfuseBlock1Word0) + if err != nil { + return nil, err + } + word1, err := f.ReadRegister(esp32h2EfuseBlock1Word0 + 4) + if err != nil { + return nil, err + } + return decodeEfuseMAC(word0, word1), nil +} + +// esp32h2ReadChipRevision reads the eFuse-encoded silicon revision. Note +// the minor version is a 3-bit mask here, unlike C6's 4-bit mask. +// Reference: esptool/targets/esp32h2.py get_major_chip_version()/ +// get_minor_chip_version(). +func esp32h2ReadChipRevision(f *Flasher) (ChipRevision, error) { + word3, err := f.ReadRegister(esp32h2EfuseBlock1Word3) + if err != nil { + return ChipRevision{}, err + } + major := (word3 >> 21) & 0x3 + minor := (word3 >> 18) & 0x7 + return ChipRevision{Major: int(major), Minor: int(minor)}, nil +} + +// esp32h2ReadChipFeatures returns the chip feature list. ESP32-H2 has no +// runtime-detectable flash/PSRAM eFuse bits in esptool, so this is a fixed +// list. Reference: esptool/targets/esp32h2.py get_chip_features(). +func esp32h2ReadChipFeatures(f *Flasher) ([]string, error) { + return []string{"BT 5 (LE)", "IEEE802.15.4", "Single Core", "96MHz"}, nil +} diff --git a/pkg/espflasher/target_esp32h2_test.go b/pkg/espflasher/target_esp32h2_test.go index a763d22..c6c5bc3 100644 --- a/pkg/espflasher/target_esp32h2_test.go +++ b/pkg/espflasher/target_esp32h2_test.go @@ -59,6 +59,71 @@ func TestESP32H2PostConnectUART(t *testing.T) { assert.False(t, f.usesUSB, "usesUSB should be false for UART") } +func TestESP32H2MAC(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + switch addr { + case esp32h2EfuseBlock1Word0: + return 0x60559ff7, nil + case esp32h2EfuseBlock1Word0 + 4: + return 0x00002ca2, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32H2} + + mac, err := f.MAC() + require.NoError(t, err) + assert.Equal(t, "2c:a2:60:55:9f:f7", mac.String()) +} + +func TestESP32H2ChipRevision(t *testing.T) { + tests := []struct { + name string + word3 uint32 + want ChipRevision + }{ + {"v0.0", 0x00000000, ChipRevision{0, 0}}, + {"v1.1", (1 << 21) | (1 << 18), ChipRevision{1, 1}}, + {"max", (3 << 21) | (0x7 << 18), ChipRevision{3, 7}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + if addr == esp32h2EfuseBlock1Word3 { + return tt.word3, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32H2} + got, err := f.ChipRevision() + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestESP32H2ChipFeatures(t *testing.T) { + mc := &mockConnection{} + f := &Flasher{conn: mc, chip: defESP32H2} + got, err := f.ChipFeatures() + require.NoError(t, err) + assert.Equal(t, []string{"BT 5 (LE)", "IEEE802.15.4", "Single Core", "96MHz"}, got) +} + +func TestESP32H2ReadErrorsPropagate(t *testing.T) { + newF := func(readReg func(addr uint32) (uint32, error)) *Flasher { + return &Flasher{conn: &mockConnection{readRegFunc: readReg}, chip: defESP32H2} + } + assertRegisterErrorsPropagate(t, newF, []uint32{esp32h2EfuseBlock1Word0, esp32h2EfuseBlock1Word0 + 4}, + func(f *Flasher) error { _, err := f.MAC(); return err }) + assertRegisterErrorsPropagate(t, newF, []uint32{esp32h2EfuseBlock1Word3}, + func(f *Flasher) error { _, err := f.ChipRevision(); return err }) +} + func TestESP32H2PostConnectSecureMode(t *testing.T) { // Simulate read error (secure download mode) mc := &mockConnection{ diff --git a/pkg/espflasher/target_esp32p4_rev1.go b/pkg/espflasher/target_esp32p4_rev1.go index 3510667..c70dc5b 100644 --- a/pkg/espflasher/target_esp32p4_rev1.go +++ b/pkg/espflasher/target_esp32p4_rev1.go @@ -1,22 +1,32 @@ package espflasher +import "net" + // ESP32-P4 Rev1 (ECO2, chip revision < 3.0) register addresses for USB // interface detection and watchdog control. // Reference: esptool/targets/esp32p4.py (P4-specific values) and // esptool/targets/esp32.py (ESP32ROM base-class SPI defaults P4 inherits). // // UARTDEV_BUF_NO is revision-dependent in esptool: -// rev < 3.0 (ECO2, this target): 0x4FF3FEB0 + 24 = 0x4FF3FEC8 -// rev >= 3.0 (production, future): 0x4FFBFEB0 + 24 = 0x4FFBFEC8 +// +// rev < 3.0 (ECO2, this target): 0x4FF3FEB0 + 24 = 0x4FF3FEC8 +// rev >= 3.0 (production, future): 0x4FFBFEB0 + 24 = 0x4FFBFEC8 +// // The USB-JTAG/Serial sentinel is 6 on P4 (not 3 like C5/C6/H2). const ( - esp32p4Rev1UARTDevBufNo uint32 = 0x4FF3FEC8 - esp32p4UARTDevBufNoUSBJTAGSerial uint32 = 6 + esp32p4Rev1UARTDevBufNo uint32 = 0x4FF3FEC8 + esp32p4UARTDevBufNoUSBJTAGSerial uint32 = 6 esp32p4LPWDTConfig0 uint32 = 0x50116000 esp32p4LPWDTWProtect uint32 = 0x50116018 esp32p4LPWDTSWDConf uint32 = 0x5011601C esp32p4LPWDTSWDWProtect uint32 = 0x50116020 + + // EFUSE_BLOCK1 words used for MAC/revision decoding. + // Reference: esptool/targets/esp32p4.py (EFUSE_BLOCK1_ADDR, MAC_EFUSE_REG, + // get_major_chip_version/get_minor_chip_version). + esp32p4EfuseBlock1Word0 uint32 = 0x5012D044 // MAC_EFUSE_REG (num_word 0) + esp32p4EfuseBlock1Word2 uint32 = 0x5012D04C // num_word 2 ) // ESP32-P4 Rev1 target definition. @@ -62,6 +72,10 @@ var defESP32P4Rev1 = &chipDef{ FlashSizes: defaultFlashSizes(), PostConnect: esp32p4Rev1PostConnect, + + ReadMAC: esp32p4Rev1ReadMAC, + ReadChipRevision: esp32p4Rev1ReadChipRevision, + ReadChipFeatures: esp32p4Rev1ReadChipFeatures, } func esp32p4Rev1PostConnect(f *Flasher) error { @@ -78,3 +92,39 @@ func esp32p4Rev1PostConnect(f *Flasher) error { return nil } + +// esp32p4Rev1ReadMAC reads the factory-programmed base MAC from eFuse. +// Reference: esptool/targets/esp32p4.py read_mac(). +func esp32p4Rev1ReadMAC(f *Flasher) (net.HardwareAddr, error) { + word0, err := f.ReadRegister(esp32p4EfuseBlock1Word0) + if err != nil { + return nil, err + } + word1, err := f.ReadRegister(esp32p4EfuseBlock1Word0 + 4) + if err != nil { + return nil, err + } + return decodeEfuseMAC(word0, word1), nil +} + +// esp32p4Rev1ReadChipRevision reads the eFuse-encoded silicon revision. +// The major version is split: bit 2 comes from eFuse bit 23, bits 1:0 come +// from eFuse bits 5:4. +// Reference: esptool/targets/esp32p4.py get_major_chip_version()/ +// get_minor_chip_version(). +func esp32p4Rev1ReadChipRevision(f *Flasher) (ChipRevision, error) { + word2, err := f.ReadRegister(esp32p4EfuseBlock1Word2) + if err != nil { + return ChipRevision{}, err + } + major := (((word2 >> 23) & 0x1) << 2) | ((word2 >> 4) & 0x3) + minor := word2 & 0xF + return ChipRevision{Major: int(major), Minor: int(minor)}, nil +} + +// esp32p4Rev1ReadChipFeatures returns the chip feature list. ESP32-P4 has +// no runtime-detectable flash/PSRAM eFuse bits in esptool, so this is a +// fixed list. Reference: esptool/targets/esp32p4.py get_chip_features(). +func esp32p4Rev1ReadChipFeatures(f *Flasher) ([]string, error) { + return []string{"Dual Core + LP Core", "400MHz"}, nil +} diff --git a/pkg/espflasher/target_esp32p4_rev1_test.go b/pkg/espflasher/target_esp32p4_rev1_test.go index bc98c24..aecaf8d 100644 --- a/pkg/espflasher/target_esp32p4_rev1_test.go +++ b/pkg/espflasher/target_esp32p4_rev1_test.go @@ -53,6 +53,71 @@ func TestESP32P4Rev1PostConnectUART(t *testing.T) { assert.False(t, f.usesUSB, "usesUSB should be false for UART") } +func TestESP32P4Rev1MAC(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + switch addr { + case esp32p4EfuseBlock1Word0: + return 0x60559ff7, nil + case esp32p4EfuseBlock1Word0 + 4: + return 0x00002ca2, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32P4Rev1} + + mac, err := f.MAC() + require.NoError(t, err) + assert.Equal(t, "2c:a2:60:55:9f:f7", mac.String()) +} + +func TestESP32P4Rev1ChipRevision(t *testing.T) { + tests := []struct { + name string + word2 uint32 + want ChipRevision + }{ + {"v0.0", 0x00000000, ChipRevision{0, 0}}, + {"v1.1", (1 << 4) | 1, ChipRevision{1, 1}}, + {"v5.15 (major bit2 set)", (1 << 23) | 1<<4 | 0xF, ChipRevision{5, 15}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + if addr == esp32p4EfuseBlock1Word2 { + return tt.word2, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32P4Rev1} + got, err := f.ChipRevision() + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestESP32P4Rev1ChipFeatures(t *testing.T) { + mc := &mockConnection{} + f := &Flasher{conn: mc, chip: defESP32P4Rev1} + got, err := f.ChipFeatures() + require.NoError(t, err) + assert.Equal(t, []string{"Dual Core + LP Core", "400MHz"}, got) +} + +func TestESP32P4Rev1ReadErrorsPropagate(t *testing.T) { + newF := func(readReg func(addr uint32) (uint32, error)) *Flasher { + return &Flasher{conn: &mockConnection{readRegFunc: readReg}, chip: defESP32P4Rev1} + } + assertRegisterErrorsPropagate(t, newF, []uint32{esp32p4EfuseBlock1Word0, esp32p4EfuseBlock1Word0 + 4}, + func(f *Flasher) error { _, err := f.MAC(); return err }) + assertRegisterErrorsPropagate(t, newF, []uint32{esp32p4EfuseBlock1Word2}, + func(f *Flasher) error { _, err := f.ChipRevision(); return err }) +} + func TestESP32P4Rev1PostConnectSecureMode(t *testing.T) { // Simulate read error (secure download mode) mc := &mockConnection{ diff --git a/pkg/espflasher/target_esp32s2.go b/pkg/espflasher/target_esp32s2.go index 0aec549..bf565e3 100644 --- a/pkg/espflasher/target_esp32s2.go +++ b/pkg/espflasher/target_esp32s2.go @@ -2,6 +2,7 @@ package espflasher import ( "fmt" + "net" "time" ) @@ -41,6 +42,15 @@ const ( esp32s2GPIOStrapSPIBootMask uint32 = 1 << 3 esp32s2RTCCntlOption1Reg uint32 = 0x3F408128 esp32s2RTCCntlForceDownloadBoot uint32 = 0x1 + + // EFUSE_BLOCK1/BLOCK2 words used for MAC/revision/feature decoding. + // Reference: esptool/targets/esp32s2.py (EFUSE_BLOCK1_ADDR, + // EFUSE_BLOCK2_ADDR, MAC_EFUSE_REG, get_major_chip_version/ + // get_minor_chip_version/get_flash_cap/get_psram_cap/get_block2_version). + esp32s2EfuseBlock1Word0 uint32 = 0x3F41A044 // MAC_EFUSE_REG (num_word 0) + esp32s2EfuseBlock1Word3 uint32 = 0x3F41A050 // num_word 3 + esp32s2EfuseBlock1Word4 uint32 = 0x3F41A054 // num_word 4 + esp32s2EfuseBlock2Word4 uint32 = 0x3F41A06C // num_word 4 (BLK_VERSION_MINOR) ) // ESP32-S2 target definition. @@ -87,6 +97,10 @@ var defESP32S2 = &chipDef{ PostConnect: esp32s2PostConnect, HardResetOTG: esp32s2HardReset, + + ReadMAC: esp32s2ReadMAC, + ReadChipRevision: esp32s2ReadChipRevision, + ReadChipFeatures: esp32s2ReadChipFeatures, } // esp32s2PostConnect detects the USB interface type. @@ -168,3 +182,88 @@ func esp32s2WatchdogReset(f *Flasher) error { return nil } + +// esp32s2ReadMAC reads the factory-programmed base MAC from eFuse. +// Reference: esptool/targets/esp32s2.py read_mac(). +func esp32s2ReadMAC(f *Flasher) (net.HardwareAddr, error) { + word0, err := f.ReadRegister(esp32s2EfuseBlock1Word0) + if err != nil { + return nil, err + } + word1, err := f.ReadRegister(esp32s2EfuseBlock1Word0 + 4) + if err != nil { + return nil, err + } + return decodeEfuseMAC(word0, word1), nil +} + +// esp32s2ReadChipRevision reads the eFuse-encoded silicon revision. +// Reference: esptool/targets/esp32s2.py get_major_chip_version()/ +// get_minor_chip_version(). +func esp32s2ReadChipRevision(f *Flasher) (ChipRevision, error) { + word3, err := f.ReadRegister(esp32s2EfuseBlock1Word3) + if err != nil { + return ChipRevision{}, err + } + word4, err := f.ReadRegister(esp32s2EfuseBlock1Word4) + if err != nil { + return ChipRevision{}, err + } + major := (word3 >> 18) & 0x3 + hi := (word3 >> 20) & 0x1 + low := (word4 >> 4) & 0x7 + minor := (hi << 3) + low + return ChipRevision{Major: int(major), Minor: int(minor)}, nil +} + +// esp32s2ReadChipFeatures returns the chip feature list. +// Reference: esptool/targets/esp32s2.py get_chip_features(). +func esp32s2ReadChipFeatures(f *Flasher) ([]string, error) { + word3, err := f.ReadRegister(esp32s2EfuseBlock1Word3) + if err != nil { + return nil, err + } + block2Word4, err := f.ReadRegister(esp32s2EfuseBlock2Word4) + if err != nil { + return nil, err + } + + flashCap := (word3 >> 21) & 0xF + flashVersion, ok := map[uint32]string{ + 0: "No Embedded Flash", + 1: "Embedded Flash 2MB", + 2: "Embedded Flash 4MB", + }[flashCap] + if !ok { + flashVersion = "Unknown Embedded Flash" + } + + psramCap := (word3 >> 28) & 0xF + psramVersion, ok := map[uint32]string{ + 0: "No Embedded PSRAM", + 1: "Embedded PSRAM 2MB", + 2: "Embedded PSRAM 4MB", + }[psramCap] + if !ok { + psramVersion = "Unknown Embedded PSRAM" + } + + block2Version := (block2Word4 >> 4) & 0x7 + block2VersionStr, ok := map[uint32]string{ + 0: "No calibration in BLK2 of efuse", + 1: "ADC and temperature sensor calibration in BLK2 of eFuse V1", + 2: "ADC and temperature sensor calibration in BLK2 of eFuse V2", + }[block2Version] + if !ok { + block2VersionStr = "Unknown calibration in BLK2" + } + + return []string{ + "Wi-Fi", + "Single Core", + "240MHz", + flashVersion, + psramVersion, + block2VersionStr, + }, nil +} diff --git a/pkg/espflasher/target_esp32s2_test.go b/pkg/espflasher/target_esp32s2_test.go index c8666c6..a8b8f88 100644 --- a/pkg/espflasher/target_esp32s2_test.go +++ b/pkg/espflasher/target_esp32s2_test.go @@ -60,6 +60,113 @@ func TestESP32S2PostConnectSecureMode(t *testing.T) { assert.False(t, f.usesUSB, "should default to non-USB on read error") } +func TestESP32S2MAC(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + switch addr { + case esp32s2EfuseBlock1Word0: + return 0x60559ff7, nil + case esp32s2EfuseBlock1Word0 + 4: + return 0x00002ca2, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32S2} + + mac, err := f.MAC() + require.NoError(t, err) + assert.Equal(t, "2c:a2:60:55:9f:f7", mac.String()) +} + +func TestESP32S2ChipRevision(t *testing.T) { + tests := []struct { + name string + word3 uint32 + word4 uint32 + want ChipRevision + }{ + {"v0.0", 0x00000000, 0x00000000, ChipRevision{0, 0}}, + {"v1.1", 1 << 18, 1 << 4, ChipRevision{1, 1}}, + {"max", (3 << 18) | (1 << 20), 0x7 << 4, ChipRevision{3, 15}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + switch addr { + case esp32s2EfuseBlock1Word3: + return tt.word3, nil + case esp32s2EfuseBlock1Word4: + return tt.word4, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32S2} + got, err := f.ChipRevision() + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestESP32S2ChipFeatures(t *testing.T) { + tests := []struct { + name string + word3 uint32 + block2word4 uint32 + want []string + }{ + { + "defaults", + 0, 0, + []string{"Wi-Fi", "Single Core", "240MHz", "No Embedded Flash", "No Embedded PSRAM", "No calibration in BLK2 of efuse"}, + }, + { + "4MB flash, 4MB psram, V2 calib", + (2 << 21) | (2 << 28), 2 << 4, + []string{"Wi-Fi", "Single Core", "240MHz", "Embedded Flash 4MB", "Embedded PSRAM 4MB", "ADC and temperature sensor calibration in BLK2 of eFuse V2"}, + }, + { + "unknown values", + (0xF << 21) | (0xF << 28), 0x7 << 4, + []string{"Wi-Fi", "Single Core", "240MHz", "Unknown Embedded Flash", "Unknown Embedded PSRAM", "Unknown calibration in BLK2"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + switch addr { + case esp32s2EfuseBlock1Word3: + return tt.word3, nil + case esp32s2EfuseBlock2Word4: + return tt.block2word4, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32S2} + got, err := f.ChipFeatures() + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestESP32S2ReadErrorsPropagate(t *testing.T) { + newF := func(readReg func(addr uint32) (uint32, error)) *Flasher { + return &Flasher{conn: &mockConnection{readRegFunc: readReg}, chip: defESP32S2} + } + assertRegisterErrorsPropagate(t, newF, []uint32{esp32s2EfuseBlock1Word0, esp32s2EfuseBlock1Word0 + 4}, + func(f *Flasher) error { _, err := f.MAC(); return err }) + assertRegisterErrorsPropagate(t, newF, []uint32{esp32s2EfuseBlock1Word3, esp32s2EfuseBlock1Word4}, + func(f *Flasher) error { _, err := f.ChipRevision(); return err }) + assertRegisterErrorsPropagate(t, newF, []uint32{esp32s2EfuseBlock1Word3, esp32s2EfuseBlock2Word4}, + func(f *Flasher) error { _, err := f.ChipFeatures(); return err }) +} + // TestESP32S2HardResetWatchdogSequence verifies that, when USB-OTG was // detected and the strap/force-download gate is clear, esp32s2HardReset // issues the exact unlock->config1->config0->relock RTC watchdog sequence diff --git a/pkg/espflasher/target_esp32s3.go b/pkg/espflasher/target_esp32s3.go index fac659b..6f73015 100644 --- a/pkg/espflasher/target_esp32s3.go +++ b/pkg/espflasher/target_esp32s3.go @@ -1,6 +1,9 @@ package espflasher -import "fmt" +import ( + "fmt" + "net" +) // ESP32-S3 register addresses for USB interface detection and watchdog control. // Reference: esptool/targets/esp32s3.py @@ -17,6 +20,17 @@ const ( esp32s3RTCCntlSWDAutoFeedEn uint32 = 1 << 31 esp32s3RTCCntlSWDWProtect uint32 = 0x600080B8 esp32s3RTCCntlSWDWKey uint32 = 0x8F1D312A + + // EFUSE_BLOCK1/BLOCK2 words used for MAC/revision/feature decoding. + // Reference: esptool/targets/esp32s3.py (EFUSE_BLOCK1_ADDR, + // EFUSE_BLOCK2_ADDR, MAC_EFUSE_REG, get_major_chip_version/ + // get_minor_chip_version/get_blk_version_major/get_blk_version_minor/ + // get_flash_cap/get_flash_vendor/get_psram_cap/get_psram_vendor). + esp32s3EfuseBlock1Word0 uint32 = 0x60007044 // MAC_EFUSE_REG (num_word 0) + esp32s3EfuseBlock1Word3 uint32 = 0x60007050 // num_word 3 + esp32s3EfuseBlock1Word4 uint32 = 0x60007054 // num_word 4 + esp32s3EfuseBlock1Word5 uint32 = 0x60007058 // num_word 5 + esp32s3EfuseBlock2Word4 uint32 = 0x6000706C // num_word 4 (BLK_VERSION_MAJOR) ) // ESP32-S3 target definition. @@ -60,6 +74,10 @@ var defESP32S3 = &chipDef{ FlashSizes: defaultFlashSizes(), PostConnect: esp32s3PostConnect, + + ReadMAC: esp32s3ReadMAC, + ReadChipRevision: esp32s3ReadChipRevision, + ReadChipFeatures: esp32s3ReadChipFeatures, } // esp32s3PostConnect detects the USB interface type and disables watchdogs @@ -123,3 +141,120 @@ func disableWatchdogsESP32S3(f *Flasher) error { return nil } + +// esp32s3ReadMAC reads the factory-programmed base MAC from eFuse. +// Reference: esptool/targets/esp32s3.py read_mac(). +func esp32s3ReadMAC(f *Flasher) (net.HardwareAddr, error) { + word0, err := f.ReadRegister(esp32s3EfuseBlock1Word0) + if err != nil { + return nil, err + } + word1, err := f.ReadRegister(esp32s3EfuseBlock1Word0 + 4) + if err != nil { + return nil, err + } + return decodeEfuseMAC(word0, word1), nil +} + +// esp32s3IsEco0 mirrors esptool's ESP32S3ROM.is_eco0(): on v0.0 (ECO0) +// silicon the major-version field was reallocated for other purposes when +// the eFuse block version is v1.1, so the raw major/minor fields must be +// overridden to 0/0 rather than trusted directly. +// Reference: esptool/targets/esp32s3.py is_eco0()/get_major_chip_version()/ +// get_minor_chip_version(). +func esp32s3IsEco0(rawMinor, blkVerMajor, blkVerMinor uint32) bool { + return (rawMinor&0x7) == 0 && blkVerMajor == 1 && blkVerMinor == 1 +} + +// esp32s3ReadChipRevision reads the eFuse-encoded silicon revision, +// including the ECO0 workaround (see esp32s3IsEco0). +// Reference: esptool/targets/esp32s3.py get_major_chip_version()/ +// get_minor_chip_version()/get_raw_major_chip_version()/ +// get_raw_minor_chip_version()/get_blk_version_major()/get_blk_version_minor(). +func esp32s3ReadChipRevision(f *Flasher) (ChipRevision, error) { + word3, err := f.ReadRegister(esp32s3EfuseBlock1Word3) + if err != nil { + return ChipRevision{}, err + } + word5, err := f.ReadRegister(esp32s3EfuseBlock1Word5) + if err != nil { + return ChipRevision{}, err + } + block2Word4, err := f.ReadRegister(esp32s3EfuseBlock2Word4) + if err != nil { + return ChipRevision{}, err + } + + hi := (word5 >> 23) & 0x1 + low := (word3 >> 18) & 0x7 + rawMinor := (hi << 3) + low + rawMajor := (word5 >> 24) & 0x3 + + blkVerMajor := block2Word4 & 0x3 + blkVerMinor := (word3 >> 24) & 0x7 + + if esp32s3IsEco0(rawMinor, blkVerMajor, blkVerMinor) { + return ChipRevision{Major: 0, Minor: 0}, nil + } + return ChipRevision{Major: int(rawMajor), Minor: int(rawMinor)}, nil +} + +// esp32s3ReadChipFeatures returns the chip feature list. +// Reference: esptool/targets/esp32s3.py get_chip_features(). +func esp32s3ReadChipFeatures(f *Flasher) ([]string, error) { + word3, err := f.ReadRegister(esp32s3EfuseBlock1Word3) + if err != nil { + return nil, err + } + word4, err := f.ReadRegister(esp32s3EfuseBlock1Word4) + if err != nil { + return nil, err + } + word5, err := f.ReadRegister(esp32s3EfuseBlock1Word5) + if err != nil { + return nil, err + } + + features := []string{"Wi-Fi", "BT 5 (LE)", "Dual Core + LP Core", "240MHz"} + + flashCap := (word3 >> 27) & 0x7 + flash, ok := map[uint32]string{ + 1: "Embedded Flash 8MB", + 2: "Embedded Flash 4MB", + }[flashCap] + if !ok && flashCap != 0 { + flash = "Unknown Embedded Flash" + } + if flash != "" { + vendorID := word4 & 0x7 + vendor := map[uint32]string{ + 1: "XMC", + 2: "GD", + 3: "FM", + 4: "TT", + 5: "BY", + }[vendorID] + features = append(features, fmt.Sprintf("%s (%s)", flash, vendor)) + } + + psramCap := ((word4 >> 3) & 0x3) | (((word5 >> 19) & 0x1) << 2) + psram, ok := map[uint32]string{ + 1: "Embedded PSRAM 8MB", + 2: "Embedded PSRAM 2MB", + 3: "Embedded PSRAM 16MB", + 4: "Embedded PSRAM 4MB", + }[psramCap] + if !ok && psramCap != 0 { + psram = "Unknown Embedded PSRAM" + } + if psram != "" { + vendorID := (word4 >> 7) & 0x3 + vendor := map[uint32]string{ + 1: "AP_3v3", + 2: "AP_1v8", + }[vendorID] + features = append(features, fmt.Sprintf("%s (%s)", psram, vendor)) + } + + return features, nil +} diff --git a/pkg/espflasher/target_esp32s3_test.go b/pkg/espflasher/target_esp32s3_test.go index 7bcad4b..8174ab6 100644 --- a/pkg/espflasher/target_esp32s3_test.go +++ b/pkg/espflasher/target_esp32s3_test.go @@ -75,3 +75,147 @@ func TestESP32S3PostConnectUART(t *testing.T) { t.Error("usesUSB should be false for UART") } } + +func TestESP32S3MAC(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + switch addr { + case esp32s3EfuseBlock1Word0: + return 0x60559ff7, nil + case esp32s3EfuseBlock1Word0 + 4: + return 0x00002ca2, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32S3} + + mac, err := f.MAC() + if err != nil { + t.Fatalf("MAC() error: %v", err) + } + if want := "2c:a2:60:55:9f:f7"; mac.String() != want { + t.Errorf("MAC() = %s, want %s", mac, want) + } +} + +func TestESP32S3ChipRevision(t *testing.T) { + tests := []struct { + name string + word3 uint32 + word5 uint32 + block2word4 uint32 + want ChipRevision + }{ + { + name: "v0.0 non-eco0 path (blk_ver 1.0)", + word3: 0, word5: 0, block2word4: 1, + want: ChipRevision{0, 0}, + }, + { + name: "v1.1", + word3: 1 << 18, word5: 1 << 24, block2word4: 0, + want: ChipRevision{1, 1}, + }, + { + name: "eco0 override: raw minor low3=0, blk_ver 1.1 -> forced v0.0", + // raw_minor low bits (word3>>18)&0x7 = 0, hi (word5>>23)&1 = 0 -> raw_minor = 0 + // blk_ver_major (block2word4&0x3) = 1, blk_ver_minor ((word3>>24)&0x7) = 1 + word3: 1 << 24, word5: 2 << 24 /* raw_major=2, irrelevant once eco0 fires */, block2word4: 1, + want: ChipRevision{0, 0}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + switch addr { + case esp32s3EfuseBlock1Word3: + return tt.word3, nil + case esp32s3EfuseBlock1Word5: + return tt.word5, nil + case esp32s3EfuseBlock2Word4: + return tt.block2word4, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32S3} + got, err := f.ChipRevision() + if err != nil { + t.Fatalf("ChipRevision() error: %v", err) + } + if got != tt.want { + t.Errorf("ChipRevision() = %+v, want %+v", got, tt.want) + } + }) + } +} + +func TestESP32S3ReadErrorsPropagate(t *testing.T) { + newF := func(readReg func(addr uint32) (uint32, error)) *Flasher { + return &Flasher{conn: &mockConnection{readRegFunc: readReg}, chip: defESP32S3} + } + assertRegisterErrorsPropagate(t, newF, []uint32{esp32s3EfuseBlock1Word0, esp32s3EfuseBlock1Word0 + 4}, + func(f *Flasher) error { _, err := f.MAC(); return err }) + assertRegisterErrorsPropagate(t, newF, []uint32{esp32s3EfuseBlock1Word3, esp32s3EfuseBlock1Word5, esp32s3EfuseBlock2Word4}, + func(f *Flasher) error { _, err := f.ChipRevision(); return err }) + assertRegisterErrorsPropagate(t, newF, []uint32{esp32s3EfuseBlock1Word3, esp32s3EfuseBlock1Word4, esp32s3EfuseBlock1Word5}, + func(f *Flasher) error { _, err := f.ChipFeatures(); return err }) +} + +func TestESP32S3ChipFeatures(t *testing.T) { + tests := []struct { + name string + word3 uint32 + word4 uint32 + word5 uint32 + want []string + }{ + { + "no flash or psram", + 0, 0, 0, + []string{"Wi-Fi", "BT 5 (LE)", "Dual Core + LP Core", "240MHz"}, + }, + { + "8MB flash TT, 2MB psram AP_1v8", + 1 << 27, (2 << 7) | (2 << 3) | 4, 0, + []string{"Wi-Fi", "BT 5 (LE)", "Dual Core + LP Core", "240MHz", "Embedded Flash 8MB (TT)", "Embedded PSRAM 2MB (AP_1v8)"}, + }, + { + "unknown flash/psram caps", + 6 << 27, (3 << 3) | 7, 1 << 19, + []string{"Wi-Fi", "BT 5 (LE)", "Dual Core + LP Core", "240MHz", "Unknown Embedded Flash ()", "Unknown Embedded PSRAM ()"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mc := &mockConnection{ + readRegFunc: func(addr uint32) (uint32, error) { + switch addr { + case esp32s3EfuseBlock1Word3: + return tt.word3, nil + case esp32s3EfuseBlock1Word4: + return tt.word4, nil + case esp32s3EfuseBlock1Word5: + return tt.word5, nil + } + return 0, nil + }, + } + f := &Flasher{conn: mc, chip: defESP32S3} + got, err := f.ChipFeatures() + if err != nil { + t.Fatalf("ChipFeatures() error: %v", err) + } + if len(got) != len(tt.want) { + t.Fatalf("ChipFeatures() = %v, want %v", got, tt.want) + } + for i := range got { + if got[i] != tt.want[i] { + t.Errorf("ChipFeatures()[%d] = %q, want %q", i, got[i], tt.want[i]) + } + } + }) + } +}