Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion pkg/espflasher/chip.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package espflasher

import "fmt"
import (
"fmt"
"net"
)

// ChipType identifies the ESP chip family.
type ChipType int
Expand Down Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions pkg/espflasher/flasher.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/hex"
"fmt"
"io"
"net"
"runtime"
"time"

Expand Down Expand Up @@ -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.
Expand Down
116 changes: 116 additions & 0 deletions pkg/espflasher/flasher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/binary"
"errors"
"fmt"
"net"
"testing"
)

Expand Down Expand Up @@ -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)
}
}
}
158 changes: 158 additions & 0 deletions pkg/espflasher/target_esp32.go
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
}
Loading
Loading