Skip to content

Commit f8a6928

Browse files
jgangemideadprogram
authored andcommitted
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
1 parent 2d40bcc commit f8a6928

21 files changed

Lines changed: 1849 additions & 13 deletions

pkg/espflasher/chip.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package espflasher
22

3-
import "fmt"
3+
import (
4+
"fmt"
5+
"net"
6+
)
47

58
// ChipType identifies the ESP chip family.
69
type ChipType int
@@ -135,6 +138,19 @@ type chipDef struct {
135138
// usesUSB/hardReset path. Nil for chips without a native-OTG reset
136139
// mechanism.
137140
HardResetOTG func(f *Flasher) bool
141+
142+
// ReadMAC reads the factory-programmed base MAC address from eFuse.
143+
// Nil for chips (ESP8266) that don't expose it via this scheme.
144+
ReadMAC func(f *Flasher) (net.HardwareAddr, error)
145+
146+
// ReadChipRevision reads the eFuse-encoded silicon revision.
147+
// Nil for chips (ESP8266) that don't expose it via this scheme.
148+
ReadChipRevision func(f *Flasher) (ChipRevision, error)
149+
150+
// ReadChipFeatures reads (or, for chips with no runtime-detectable
151+
// feature bits, returns a fixed list of) the chip's feature set.
152+
// Nil for chips (ESP8266) that don't expose it via this scheme.
153+
ReadChipFeatures func(f *Flasher) ([]string, error)
138154
}
139155

140156
// chipDetectMagicRegAddr is the register address that has a different

pkg/espflasher/flasher.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"encoding/hex"
88
"fmt"
99
"io"
10+
"net"
1011
"runtime"
1112
"time"
1213

@@ -1347,6 +1348,60 @@ func (f *Flasher) FlashID() (uint8, uint16, error) {
13471348
return mfgID, devID, nil
13481349
}
13491350

1351+
// MAC returns the factory-programmed base MAC address read from eFuse.
1352+
// Works both pre- and post-stub, since ReadRegister uses the READ_REG
1353+
// command, which is implemented by both the ROM loader and the stub.
1354+
func (f *Flasher) MAC() (net.HardwareAddr, error) {
1355+
if f.chip == nil || f.chip.ReadMAC == nil {
1356+
return nil, &UnsupportedCommandError{Command: "read MAC (chip not detected or unsupported)"}
1357+
}
1358+
return f.chip.ReadMAC(f)
1359+
}
1360+
1361+
// ChipRevision is the eFuse-encoded silicon revision.
1362+
type ChipRevision struct {
1363+
Major int
1364+
Minor int
1365+
}
1366+
1367+
// String returns the revision formatted as "vMAJOR.MINOR".
1368+
func (r ChipRevision) String() string {
1369+
return fmt.Sprintf("v%d.%d", r.Major, r.Minor)
1370+
}
1371+
1372+
// ChipRevision reads the eFuse-encoded silicon revision. Works both
1373+
// pre- and post-stub, like MAC.
1374+
func (f *Flasher) ChipRevision() (ChipRevision, error) {
1375+
if f.chip == nil || f.chip.ReadChipRevision == nil {
1376+
return ChipRevision{}, &UnsupportedCommandError{Command: "read chip revision (chip not detected or unsupported)"}
1377+
}
1378+
return f.chip.ReadChipRevision(f)
1379+
}
1380+
1381+
// ChipFeatures returns a human-readable feature list, mirroring esptool's
1382+
// get_chip_features(). Works both pre- and post-stub, like MAC.
1383+
func (f *Flasher) ChipFeatures() ([]string, error) {
1384+
if f.chip == nil || f.chip.ReadChipFeatures == nil {
1385+
return nil, &UnsupportedCommandError{Command: "read chip features (chip not detected or unsupported)"}
1386+
}
1387+
return f.chip.ReadChipFeatures(f)
1388+
}
1389+
1390+
// decodeEfuseMAC packs two adjacent 32-bit eFuse words into a 6-byte MAC
1391+
// address, mirroring esptool's read_mac(): struct.pack(">II", word1, word0)
1392+
// trimmed to the middle 6 bytes (the leading 2 bytes of word1 are CRC/other
1393+
// bits, not part of the MAC).
1394+
func decodeEfuseMAC(word0, word1 uint32) net.HardwareAddr {
1395+
return net.HardwareAddr{
1396+
byte(word1 >> 8),
1397+
byte(word1),
1398+
byte(word0 >> 24),
1399+
byte(word0 >> 16),
1400+
byte(word0 >> 8),
1401+
byte(word0),
1402+
}
1403+
}
1404+
13501405
// runSPIFlashCommand executes a SPI flash command at the register level.
13511406
// It configures the SPI peripheral to send 'cmd' as an 8-bit command,
13521407
// optionally write 'data' bytes, and read back 'readBits' bits of response.

pkg/espflasher/flasher_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/binary"
66
"errors"
77
"fmt"
8+
"net"
89
"testing"
910
)
1011

@@ -543,3 +544,118 @@ func TestGetSecurityInfo(t *testing.T) {
543544
}
544545
}
545546
}
547+
548+
func TestChipRevisionString(t *testing.T) {
549+
rev := ChipRevision{Major: 1, Minor: 2}
550+
if got, want := rev.String(), "v1.2"; got != want {
551+
t.Errorf("ChipRevision.String() = %q, want %q", got, want)
552+
}
553+
}
554+
555+
func TestMACNilChip(t *testing.T) {
556+
f := &Flasher{conn: &mockConnection{}}
557+
_, err := f.MAC()
558+
if err == nil {
559+
t.Fatal("expected error for nil chip")
560+
}
561+
if _, ok := err.(*UnsupportedCommandError); !ok {
562+
t.Errorf("expected UnsupportedCommandError, got %T: %v", err, err)
563+
}
564+
}
565+
566+
func TestMACUnsupportedChip(t *testing.T) {
567+
// ESP8266 leaves ReadMAC nil.
568+
f := &Flasher{conn: &mockConnection{}, chip: chipDefs[ChipESP8266]}
569+
_, err := f.MAC()
570+
if err == nil {
571+
t.Fatal("expected error for ESP8266")
572+
}
573+
if _, ok := err.(*UnsupportedCommandError); !ok {
574+
t.Errorf("expected UnsupportedCommandError, got %T: %v", err, err)
575+
}
576+
}
577+
578+
func TestChipRevisionNilChip(t *testing.T) {
579+
f := &Flasher{conn: &mockConnection{}}
580+
_, err := f.ChipRevision()
581+
if err == nil {
582+
t.Fatal("expected error for nil chip")
583+
}
584+
if _, ok := err.(*UnsupportedCommandError); !ok {
585+
t.Errorf("expected UnsupportedCommandError, got %T: %v", err, err)
586+
}
587+
}
588+
589+
func TestChipRevisionUnsupportedChip(t *testing.T) {
590+
f := &Flasher{conn: &mockConnection{}, chip: chipDefs[ChipESP8266]}
591+
_, err := f.ChipRevision()
592+
if err == nil {
593+
t.Fatal("expected error for ESP8266")
594+
}
595+
if _, ok := err.(*UnsupportedCommandError); !ok {
596+
t.Errorf("expected UnsupportedCommandError, got %T: %v", err, err)
597+
}
598+
}
599+
600+
func TestChipFeaturesNilChip(t *testing.T) {
601+
f := &Flasher{conn: &mockConnection{}}
602+
_, err := f.ChipFeatures()
603+
if err == nil {
604+
t.Fatal("expected error for nil chip")
605+
}
606+
if _, ok := err.(*UnsupportedCommandError); !ok {
607+
t.Errorf("expected UnsupportedCommandError, got %T: %v", err, err)
608+
}
609+
}
610+
611+
func TestChipFeaturesUnsupportedChip(t *testing.T) {
612+
f := &Flasher{conn: &mockConnection{}, chip: chipDefs[ChipESP8266]}
613+
_, err := f.ChipFeatures()
614+
if err == nil {
615+
t.Fatal("expected error for ESP8266")
616+
}
617+
if _, ok := err.(*UnsupportedCommandError); !ok {
618+
t.Errorf("expected UnsupportedCommandError, got %T: %v", err, err)
619+
}
620+
}
621+
622+
func TestMACDispatchesToChip(t *testing.T) {
623+
f := &Flasher{conn: &mockConnection{}, chip: chipDefs[ChipESP32C3]}
624+
mock := f.conn.(*mockConnection)
625+
mock.readRegFunc = func(addr uint32) (uint32, error) {
626+
switch addr {
627+
case esp32c3EfuseBlock1Word0:
628+
return 0x01020304, nil
629+
case esp32c3EfuseBlock1Word0 + 4:
630+
return 0x00000506, nil
631+
}
632+
return 0, nil
633+
}
634+
mac, err := f.MAC()
635+
if err != nil {
636+
t.Fatalf("MAC() failed: %v", err)
637+
}
638+
want := net.HardwareAddr{0x05, 0x06, 0x01, 0x02, 0x03, 0x04}
639+
if mac.String() != want.String() {
640+
t.Errorf("MAC() = %s, want %s", mac, want)
641+
}
642+
}
643+
644+
// assertRegisterErrorsPropagate verifies that call returns a non-nil error
645+
// when any single register in addrs fails to read, one at a time (all
646+
// others succeed with 0). This proves every ReadRegister error-check branch
647+
// in the decoder under test is reachable, not just the first.
648+
func assertRegisterErrorsPropagate(t *testing.T, newFlasher func(readReg func(addr uint32) (uint32, error)) *Flasher, addrs []uint32, call func(f *Flasher) error) {
649+
t.Helper()
650+
for _, failAddr := range addrs {
651+
f := newFlasher(func(addr uint32) (uint32, error) {
652+
if addr == failAddr {
653+
return 0, errors.New("register read failed")
654+
}
655+
return 0, nil
656+
})
657+
if err := call(f); err == nil {
658+
t.Errorf("expected error when register 0x%08X fails to read", failAddr)
659+
}
660+
}
661+
}

pkg/espflasher/target_esp32.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,33 @@
11
package espflasher
22

3+
import (
4+
"fmt"
5+
"net"
6+
)
7+
8+
// ESP32 (classic) register addresses used for MAC/revision/feature
9+
// decoding. Unlike every later chip, the classic ESP32's efuse layout has
10+
// no BLOCK1; words are read directly off EFUSE_RD_REG_BASE, and the major
11+
// chip revision isn't a bitfield — it's a 3-bit value assembled from two
12+
// efuse bits plus one bit from an entirely separate SYSCON register,
13+
// looked up in a table.
14+
// Reference: esptool/targets/esp32.py (EFUSE_RD_REG_BASE, read_efuse(),
15+
// APB_CTL_DATE_ADDR, get_major_chip_version/get_minor_chip_version/
16+
// get_pkg_version/get_chip_features).
17+
const (
18+
esp32EfuseWord1 uint32 = 0x3FF5A004 // read_efuse(1)
19+
esp32EfuseWord2 uint32 = 0x3FF5A008 // read_efuse(2)
20+
esp32EfuseWord3 uint32 = 0x3FF5A00C // read_efuse(3)
21+
esp32EfuseWord4 uint32 = 0x3FF5A010 // read_efuse(4)
22+
esp32EfuseWord5 uint32 = 0x3FF5A014 // read_efuse(5)
23+
esp32EfuseWord6 uint32 = 0x3FF5A018 // read_efuse(6)
24+
25+
// APB_CTL_DATE_ADDR = DR_REG_SYSCON_BASE (0x3FF66000) + 0x7C. Bit 31
26+
// supplies the top bit of the 3-bit major-revision lookup index; it
27+
// lives outside the eFuse block entirely.
28+
esp32APBCtlDateReg uint32 = 0x3FF6607C
29+
)
30+
331
// ESP32 target definition.
432
// Reference: https://github.com/espressif/esptool/blob/master/esptool/targets/esp32.py
533

@@ -37,4 +65,134 @@ var defESP32 = &chipDef{
3765
},
3866

3967
FlashSizes: defaultFlashSizes(),
68+
69+
ReadMAC: esp32ReadMAC,
70+
ReadChipRevision: esp32ReadChipRevision,
71+
ReadChipFeatures: esp32ReadChipFeatures,
72+
}
73+
74+
// esp32ReadMAC reads the factory-programmed base MAC from eFuse.
75+
// Reference: esptool/targets/esp32.py read_mac().
76+
func esp32ReadMAC(f *Flasher) (net.HardwareAddr, error) {
77+
word1, err := f.ReadRegister(esp32EfuseWord1)
78+
if err != nil {
79+
return nil, err
80+
}
81+
word2, err := f.ReadRegister(esp32EfuseWord2)
82+
if err != nil {
83+
return nil, err
84+
}
85+
return decodeEfuseMAC(word1, word2), nil
86+
}
87+
88+
// esp32MajorChipVersionTable is esptool's lookup table mapping the 3-bit
89+
// combined revision-bit value (from two eFuse bits plus one SYSCON bit) to
90+
// the major chip revision. Combine values not present here (2, 4, 5, 6)
91+
// map to major revision 0.
92+
// Reference: esptool/targets/esp32.py get_major_chip_version().
93+
var esp32MajorChipVersionTable = map[uint32]int{
94+
0: 0,
95+
1: 1,
96+
3: 2,
97+
7: 3,
98+
}
99+
100+
// esp32ReadChipRevision reads the eFuse-encoded silicon revision. Unlike
101+
// every later chip, the major version isn't a bitfield: it's a lookup-table
102+
// index assembled from two eFuse bits plus one bit read from a SYSCON
103+
// register outside the eFuse block.
104+
// Reference: esptool/targets/esp32.py get_major_chip_version()/
105+
// get_minor_chip_version().
106+
func esp32ReadChipRevision(f *Flasher) (ChipRevision, error) {
107+
word3, err := f.ReadRegister(esp32EfuseWord3)
108+
if err != nil {
109+
return ChipRevision{}, err
110+
}
111+
word5, err := f.ReadRegister(esp32EfuseWord5)
112+
if err != nil {
113+
return ChipRevision{}, err
114+
}
115+
apbCtlDate, err := f.ReadRegister(esp32APBCtlDateReg)
116+
if err != nil {
117+
return ChipRevision{}, err
118+
}
119+
120+
revBit0 := (word3 >> 15) & 0x1
121+
revBit1 := (word5 >> 20) & 0x1
122+
revBit2 := (apbCtlDate >> 31) & 0x1
123+
combine := (revBit2 << 2) | (revBit1 << 1) | revBit0
124+
125+
major := esp32MajorChipVersionTable[combine] // default (unlisted) is 0
126+
minor := (word5 >> 24) & 0x3
127+
128+
return ChipRevision{Major: major, Minor: int(minor)}, nil
129+
}
130+
131+
// esp32CodingSchemeNames is esptool's literal mapping for the flash
132+
// encoding-coding-scheme feature string.
133+
// Reference: esptool/targets/esp32.py get_chip_features().
134+
var esp32CodingSchemeNames = map[uint32]string{
135+
0: "None",
136+
1: "3/4",
137+
2: "Repeat (UNSUPPORTED)",
138+
3: "None (may contain encoding data)",
139+
}
140+
141+
// esp32ReadChipFeatures returns the chip feature list.
142+
// Reference: esptool/targets/esp32.py get_chip_features().
143+
func esp32ReadChipFeatures(f *Flasher) ([]string, error) {
144+
word3, err := f.ReadRegister(esp32EfuseWord3)
145+
if err != nil {
146+
return nil, err
147+
}
148+
word4, err := f.ReadRegister(esp32EfuseWord4)
149+
if err != nil {
150+
return nil, err
151+
}
152+
word6, err := f.ReadRegister(esp32EfuseWord6)
153+
if err != nil {
154+
return nil, err
155+
}
156+
157+
features := []string{"Wi-Fi"}
158+
159+
if word3&(1<<1) == 0 {
160+
features = append(features, "BT")
161+
}
162+
163+
if word3&(1<<0) != 0 {
164+
features = append(features, "Single Core + LP Core")
165+
} else {
166+
features = append(features, "Dual Core + LP Core")
167+
}
168+
169+
if word3&(1<<13) != 0 {
170+
if word3&(1<<12) != 0 {
171+
features = append(features, "160MHz")
172+
} else {
173+
features = append(features, "240MHz")
174+
}
175+
}
176+
177+
pkgVersion := ((word3 >> 9) & 0x7) | (((word3 >> 2) & 0x1) << 3)
178+
switch pkgVersion {
179+
case 2, 4, 5, 6:
180+
features = append(features, "Embedded Flash")
181+
}
182+
if pkgVersion == 6 {
183+
features = append(features, "Embedded PSRAM")
184+
}
185+
186+
if adcVref := (word4 >> 8) & 0x1F; adcVref != 0 {
187+
features = append(features, "Vref calibration in eFuse")
188+
}
189+
190+
if word3>>14&0x1 != 0 {
191+
features = append(features, "BLK3 partially reserved")
192+
}
193+
194+
codingScheme := word6 & 0x3
195+
features = append(features, fmt.Sprintf("Coding Scheme %s", esp32CodingSchemeNames[codingScheme]))
196+
197+
return features, nil
40198
}

0 commit comments

Comments
 (0)