diff --git a/GNUmakefile b/GNUmakefile index 6a2b8c1cd..9a6dc7b8e 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -934,6 +934,8 @@ ifneq ($(XTENSA), 0) @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=xiao-esp32s3 examples/pwm @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=xiao-esp32s3 examples/adc + @$(MD5SUM) test.bin # esp32s3-wroom1 $(TINYGO) build -size short -o test.bin -target=esp32s3-wroom1 examples/blinkm @@ -942,6 +944,8 @@ ifneq ($(XTENSA), 0) @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=esp32s3-wroom1 examples/pwm @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=xiao-esp32s3 examples/adc + @$(MD5SUM) test.bin endif # esp32c3-supermini $(TINYGO) build -size short -o test.bin -target=esp32c3-supermini examples/blinky1 @@ -952,6 +956,8 @@ endif @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=esp32c3-supermini examples/pwm @$(MD5SUM) test.bin + $(TINYGO) build -size short -o test.bin -target=esp32c3-supermini examples/adc + @$(MD5SUM) test.bin $(TINYGO) build -size short -o test.bin -target=esp-c3-32s-kit examples/blinky1 @$(MD5SUM) test.bin diff --git a/src/examples/adc/adc.go b/src/examples/adc/adc.go index de8a7f885..3ec213f41 100644 --- a/src/examples/adc/adc.go +++ b/src/examples/adc/adc.go @@ -5,25 +5,15 @@ import ( "time" ) -// This example assumes that an analog sensor such as a rotary dial is connected to pin ADC0. -// When the dial is turned past the midway point, the built-in LED will light up. - func main() { machine.InitADC() - led := machine.LED - led.Configure(machine.PinConfig{Mode: machine.PinOutput}) - sensor := machine.ADC{machine.ADC2} sensor.Configure(machine.ADCConfig{}) for { val := sensor.Get() - if val < 0x8000 { - led.Low() - } else { - led.High() - } - time.Sleep(time.Millisecond * 100) + println(val) + time.Sleep(time.Millisecond * 500) } } diff --git a/src/machine/board_esp32c3-12f.go b/src/machine/board_esp32c3-12f.go index f023bb9d6..0988c7adc 100644 --- a/src/machine/board_esp32c3-12f.go +++ b/src/machine/board_esp32c3-12f.go @@ -30,9 +30,6 @@ const ( // ADC pins const ( - ADC0 Pin = ADC1_0 - ADC1 Pin = ADC2_0 - ADC1_0 Pin = IO0 ADC1_1 Pin = IO1 ADC1_2 Pin = IO2 diff --git a/src/machine/machine_esp32c3.go b/src/machine/machine_esp32c3.go index 7214acbb2..e5e10c326 100644 --- a/src/machine/machine_esp32c3.go +++ b/src/machine/machine_esp32c3.go @@ -27,16 +27,29 @@ const ( PinInput PinInputPullup PinInputPulldown + PinAnalog +) + +const ( + GPIO0 Pin = 0 + GPIO1 Pin = 1 + GPIO2 Pin = 2 + GPIO3 Pin = 3 + GPIO4 Pin = 4 + GPIO5 Pin = 5 + GPIO6 Pin = 6 +) + +const ( + ADC0 Pin = GPIO0 + ADC1 Pin = GPIO1 + ADC2 Pin = GPIO2 + ADC3 Pin = GPIO3 + ADC4 Pin = GPIO4 + ADC5 Pin = GPIO5 // avoid when WiFi is used. ) const ( - GPIO0 Pin = 0 - GPIO1 Pin = 1 - GPIO2 Pin = 2 - GPIO3 Pin = 3 - GPIO4 Pin = 4 - GPIO5 Pin = 5 - GPIO6 Pin = 6 GPIO7 Pin = 7 GPIO8 Pin = 8 GPIO9 Pin = 9 @@ -76,13 +89,15 @@ func (p Pin) Configure(config PinConfig) { const function = 1 // function 1 is GPIO for every pin muxConfig |= function << esp.IO_MUX_GPIO_MCU_SEL_Pos - // Make this pin an input pin (always). - muxConfig |= esp.IO_MUX_GPIO_FUN_IE + // FUN_IE: disable for PinAnalog (high-Z for ADC) + if config.Mode != PinAnalog { + muxConfig |= esp.IO_MUX_GPIO_FUN_IE + } // Set drive strength: 0 is lowest, 3 is highest. muxConfig |= 2 << esp.IO_MUX_GPIO_FUN_DRV_Pos - // Select pull mode. + // Select pull mode (no pulls for PinAnalog). if config.Mode == PinInputPullup { muxConfig |= esp.IO_MUX_GPIO_FUN_WPU } else if config.Mode == PinInputPulldown { @@ -99,7 +114,7 @@ func (p Pin) Configure(config PinConfig) { case PinOutput: // Set the 'output enable' bit. esp.GPIO.ENABLE_W1TS.Set(1 << p) - case PinInput, PinInputPullup, PinInputPulldown: + case PinInput, PinInputPullup, PinInputPulldown, PinAnalog: // Clear the 'output enable' bit. esp.GPIO.ENABLE_W1TC.Set(1 << p) } diff --git a/src/machine/machine_esp32c3_adc.go b/src/machine/machine_esp32c3_adc.go new file mode 100644 index 000000000..ec23637ed --- /dev/null +++ b/src/machine/machine_esp32c3_adc.go @@ -0,0 +1,414 @@ +//go:build esp32c3 && !m5stamp_c3 + +package machine + +import ( + "device/esp" + "errors" + "runtime/volatile" + "unsafe" +) + +const ( + // ADC attenuation values for ESP32-C3 APB_SARADC. + // 0 dB : ~0 .. 1.1 V + // 11 dB : ~0 .. 3.3 V (matches typical VDD) + atten0dB = 0 + atten11dB = 3 +) + +func InitADC() { + esp.SYSTEM.SetPERIP_RST_EN0_APB_SARADC_RST(1) + esp.SYSTEM.SetPERIP_CLK_EN0_APB_SARADC_CLK_EN(1) + esp.SYSTEM.SetPERIP_RST_EN0_APB_SARADC_RST(0) + + esp.RTC_CNTL.SetANA_CONF_SAR_I2C_PU(1) + esp.RTC_CNTL.SetSENSOR_CTRL_FORCE_XPD_SAR(1) + esp.APB_SARADC.SetCTRL_SARADC_XPD_SAR_FORCE(1) + esp.APB_SARADC.SetFSM_WAIT_SARADC_XPD_WAIT(8) + esp.APB_SARADC.SetFSM_WAIT_SARADC_RSTB_WAIT(8) + esp.APB_SARADC.SetFSM_WAIT_SARADC_STANDBY_WAIT(100) + esp.APB_SARADC.SetCLKM_CONF_CLK_SEL(2) + esp.APB_SARADC.SetCLKM_CONF_CLKM_DIV_NUM(1) + esp.APB_SARADC.SetCLKM_CONF_CLKM_DIV_B(0) + esp.APB_SARADC.SetCLKM_CONF_CLKM_DIV_A(0) + esp.APB_SARADC.SetCLKM_CONF_CLK_EN(1) + + var c adcSelfCalibration + c.calibrate() +} + +// ESP32-C3: ADC1 = GPIO0–GPIO4 (ch 0–4), ADC2 = GPIO5 (ch 0). ADC2 shares with Wi‑Fi; +// readings may be noisy when Wi‑Fi is active. +func (a ADC) Configure(config ADCConfig) error { + if a.Pin > 5 { + return errors.New("invalid ADC pin for ESP32-C3") + } + a.Pin.Configure(PinConfig{Mode: PinAnalog}) + return nil +} + +func (a ADC) Get() uint16 { + if a.Pin > 5 { + return 0 + } + adc1 := a.Pin <= 4 + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_ATTEN(atten11dB) + esp.APB_SARADC.SetINT_CLR_APB_SARADC1_DONE_INT_CLR(1) + esp.APB_SARADC.SetINT_CLR_APB_SARADC2_DONE_INT_CLR(1) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(0) + var raw uint32 + if adc1 { + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_CHANNEL(uint32(a.Pin)) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC1_ONETIME_SAMPLE(1) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(1) + for esp.APB_SARADC.GetINT_RAW_APB_SARADC1_DONE_INT_RAW() == 0 { + } + raw = esp.APB_SARADC.GetSAR1DATA_STATUS_APB_SARADC1_DATA() + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(0) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC1_ONETIME_SAMPLE(0) + } else { + // ADC2: GPIO5 = channel 0. Grant arbiter to ADC2 first, then set channel and start. + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC1_ONETIME_SAMPLE(0) + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_APB_FORCE(1) + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_GRANT_FORCE(1) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_CHANNEL(8) // (1<<3)|0 for ADC2 channel 0 + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC2_ONETIME_SAMPLE(1) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(1) + for esp.APB_SARADC.GetINT_RAW_APB_SARADC2_DONE_INT_RAW() == 0 { + } + raw = esp.APB_SARADC.GetSAR2DATA_STATUS_APB_SARADC2_DATA() + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(0) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC2_ONETIME_SAMPLE(0) + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_APB_FORCE(0) + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_GRANT_FORCE(0) + } + return uint16(raw&0xfff) << 4 +} + +// adcSelfCalibration +const ( + adcCalTimesC3 = 15 + adcCalOffsetRangeC3 = uint32(4096) + adcCalRtcMagicC3 = uint32(0xADC1C401) + adcCalInitMinC3 = uint32(1000) + adcCalInitMaxC3 = uint32(4096) + adcGndOffsetCompC3 = uint32(0) +) + +type adcSelfCalibration struct { + digiRefMv uint32 +} + +// calibrate sets ADC1/ADC2 init code from RTC or runs self-calibration (GND). +// eFuse is not used: on ESP32-C3 the ADC calibration fields in BLK2 are often unprogrammed. +func (c *adcSelfCalibration) calibrate() { + reg := regI2C{} + reg.sarEnable() + + var adc1Code uint32 + if saved, ok := c.restoreFromRTC(); ok { + adc1Code = saved + } else { + c.calSetupADC1() + reg.adc1CalibrationInit(0) + reg.adc1CalibrationPrepare(0) + adc1Code = c.calibrateUnit(reg, 0, c.readADC1) + c.saveToRTC(adc1Code) + reg.adc1CalibrationFinish(0) + } + + c.applyADC1Code(reg, adc1Code) + c.applyADC2Code(reg, adc1Code) +} + +// calSetupADC1 configures APB_SARADC for oneshot sampling on ADC1 channel 0 +// with fixed attenuation. This is used only during self‑calibration. +func (c *adcSelfCalibration) calSetupADC1() { + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_ATTEN(atten11dB) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_CHANNEL(0) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC1_ONETIME_SAMPLE(1) +} + +// calSetupADC2 configures APB_SARADC for oneshot sampling on ADC2 (GPIO5, ch 0). +// On C3, onetime_channel = (unit<<3)|channel → ADC2 ch0 = 8. +func (c *adcSelfCalibration) calSetupADC2() { + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_ATTEN(atten11dB) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_CHANNEL(8) // (1<<3)|0 for ADC2 + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_APB_FORCE(1) + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_GRANT_FORCE(1) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC2_ONETIME_SAMPLE(1) +} + +// readADC1 performs a single ADC1 conversion using the APB_SARADC +// oneshot path and returns the raw 12‑bit result (0..4095). +func (c *adcSelfCalibration) readADC1() uint32 { + esp.APB_SARADC.SetINT_CLR_APB_SARADC1_DONE_INT_CLR(1) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(0) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(1) + for esp.APB_SARADC.GetINT_RAW_APB_SARADC1_DONE_INT_RAW() == 0 { + } + raw := esp.APB_SARADC.GetSAR1DATA_STATUS_APB_SARADC1_DATA() & 0xfff + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(0) + return uint32(raw) +} + +// readADC2 performs a single ADC2 conversion and returns the raw 12‑bit result (0..4095). +func (c *adcSelfCalibration) readADC2() uint32 { + esp.APB_SARADC.SetINT_CLR_APB_SARADC2_DONE_INT_CLR(1) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(0) + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(1) + for esp.APB_SARADC.GetINT_RAW_APB_SARADC2_DONE_INT_RAW() == 0 { + } + raw := esp.APB_SARADC.GetSAR2DATA_STATUS_APB_SARADC2_DATA() & 0xfff + esp.APB_SARADC.SetONETIME_SAMPLE_SARADC_ONETIME_START(0) + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_APB_FORCE(0) + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_GRANT_FORCE(0) + return uint32(raw) +} + +func (c *adcSelfCalibration) restoreFromRTC() (uint32, bool) { + if esp.RTC_CNTL.GetSTORE0() != adcCalRtcMagicC3 { + return 0, false + } + code := esp.RTC_CNTL.GetSTORE1() + if code < adcCalInitMinC3 || code > adcCalInitMaxC3 { + return 0, false + } + return code, true +} + +func (c *adcSelfCalibration) saveToRTC(code uint32) { + if code < adcCalInitMinC3 || code > adcCalInitMaxC3 { + return + } + esp.RTC_CNTL.SetSTORE0(adcCalRtcMagicC3) + esp.RTC_CNTL.SetSTORE1(code) +} + +// applyADC1Code sets ADC1 init code and finishes calibration. +func (c *adcSelfCalibration) applyADC1Code(reg regI2C, code uint32) { + c.calSetupADC1() + reg.adc1CalibrationInit(0) + reg.adc1CalibrationPrepare(0) + reg.adc1SetCalibrationParam(0, code) + reg.adc1CalibrationFinish(0) +} + +// applyADC2Code sets ADC2 init code and finishes calibration. On C3 eFuse V1 +// there is no separate ADC2 calibration; IDF uses ADC1 init code for both units. +func (c *adcSelfCalibration) applyADC2Code(reg regI2C, code uint32) { + reg.adc1CalibrationInit(1) + reg.adc1CalibrationPrepare(1) + reg.adc1SetCalibrationParam(1, code) + reg.adc1CalibrationFinish(1) +} + +func (c *adcSelfCalibration) calibrateUnit(reg regI2C, adcN uint8, readADC func() uint32) uint32 { + var codeList [adcCalTimesC3]uint32 + var codeSum uint32 + + for rpt := 0; rpt < adcCalTimesC3; rpt++ { + codeH := adcCalOffsetRangeC3 + codeL := uint32(0) + chkCode := (codeH + codeL) / 2 + reg.adc1SetCalibrationParam(adcN, chkCode) + selfCal := readADC() + + for codeH-codeL > 1 { + if selfCal == 0 { + codeH = chkCode + } else { + codeL = chkCode + } + chkCode = (codeH + codeL) / 2 + reg.adc1SetCalibrationParam(adcN, chkCode) + selfCal = readADC() + if codeH-codeL == 1 { + chkCode++ + reg.adc1SetCalibrationParam(adcN, chkCode) + selfCal = readADC() + } + } + codeList[rpt] = chkCode + codeSum += chkCode + } + + codeL := codeList[0] + codeH := codeList[0] + for i := 0; i < adcCalTimesC3; i++ { + if codeList[i] < codeL { + codeL = codeList[i] + } + if codeList[i] > codeH { + codeH = codeList[i] + } + } + excluded := codeH + codeL + remaining := codeSum - excluded + finalCode := remaining / (adcCalTimesC3 - 2) + if remaining%(adcCalTimesC3-2) >= 4 { + finalCode++ + } + if finalCode < adcCalInitMinC3 { + finalCode = adcCalInitMinC3 + } + if finalCode > adcCalInitMaxC3 { + finalCode = adcCalInitMaxC3 + } + + reg.adc1SetCalibrationParam(adcN, finalCode) + return finalCode +} + +// regi2c + +// regI2C on ESP32‑C3 exposes the internal analog I2C bus that controls +// SAR ADC trim registers. Constants below mirror the layout from +// ESP‑IDF's soc/regi2c_saradc.h and TRM (I2C_RTC_CONFIG2 block). +const ( + // i2cSarADC/i2cSarADCHostID select the SAR ADC block on the internal bus. + i2cSarADC = uint8(0x69) + i2cSarADCHostID = uint8(0) + + // adc*_Dref* define the DREF (reference) bitfields for ADC1/ADC2. + adc1DrefAddr = uint8(0x2) + adc1DrefMSB = uint8(6) + adc1DrefLSB = uint8(4) + + adc2DrefAddr = uint8(0x5) + adc2DrefMSB = uint8(6) + adc2DrefLSB = uint8(4) + + // adc*_EncalGnd* control ENCAL_GND: route internal ground to ADC input + // during self‑calibration so that the pin is effectively disconnected. + adc1EncalGndAddr = uint8(0x7) + adc1EncalGndMSB = uint8(5) + adc1EncalGndLSB = uint8(5) + + adc2EncalGndAddr = uint8(0x7) + adc2EncalGndMSB = uint8(7) + adc2EncalGndLSB = uint8(7) + + // adc*_InitCode* hold the INIT_CODE (offset) that hardware uses to + // compensate ADC1/ADC2 offset error. + adc1InitCodeHighAddr = uint8(0x1) + adc1InitCodeHighMSB = uint8(3) + adc1InitCodeHighLSB = uint8(0) + adc1InitCodeLowAddr = uint8(0x0) + adc1InitCodeLowMSB = uint8(7) + adc1InitCodeLowLSB = uint8(0) + + adc2InitCodeHighAddr = uint8(0x4) + adc2InitCodeHighMSB = uint8(3) + adc2InitCodeHighLSB = uint8(0) + adc2InitCodeLowAddr = uint8(0x3) + adc2InitCodeLowMSB = uint8(7) + adc2InitCodeLowLSB = uint8(0) + + // ANA_CONFIG/ANA_CONFIG2: enable analog SAR I2C domain before regI2C access. + anaConfigReg = uintptr(0x6000E044) + i2cSarEnMask = uint32(1 << 18) + anaConfig2Reg = uintptr(0x6000E048) + anaSarCfg2En = uint32(1 << 16) + + // I2C_RTC_CONFIG2 master control register used by regI2C operations. + i2cMstCtrlHost = uintptr(0x6000E000) + i2cMstBusyBit = uint32(1 << 25) + i2cMstWrCntl = uint32(1 << 24) + i2cMstDataMask = uint32(0xFF << 16) + i2cMstDataShift = 16 + i2cMstTimeout = 10000 +) + +type regI2C struct{} + +// sarEnable enables the SAR analog I2C domain before any regI2C access. +func (r *regI2C) sarEnable() { + cfg := (*volatile.Register32)(unsafe.Pointer(anaConfigReg)) + cfg2 := (*volatile.Register32)(unsafe.Pointer(anaConfig2Reg)) + esp.RTC_CNTL.SetANA_CONF_SAR_I2C_PU(1) + cfg.Set(cfg.Get() &^ i2cSarEnMask) + cfg2.Set(cfg2.Get() | anaSarCfg2En) +} + +// adc1CalibrationInit sets DREF for the selected ADC unit +// before running the self‑calibration procedure. +func (r *regI2C) adc1CalibrationInit(adcN uint8) { + if adcN == 0 { + r.writeMask(i2cSarADC, i2cSarADCHostID, adc1DrefAddr, adc1DrefMSB, adc1DrefLSB, 1) + } else { + r.writeMask(i2cSarADC, i2cSarADCHostID, adc2DrefAddr, adc2DrefMSB, adc2DrefLSB, 1) + } +} + +// adc1CalibrationPrepare enables ENCAL_GND so that the ADC input +// is internally shorted to ground during self‑calibration. +func (r *regI2C) adc1CalibrationPrepare(adcN uint8) { + if adcN == 0 { + r.writeMask(i2cSarADC, i2cSarADCHostID, adc1EncalGndAddr, adc1EncalGndMSB, adc1EncalGndLSB, 1) + } else { + r.writeMask(i2cSarADC, i2cSarADCHostID, adc2EncalGndAddr, adc2EncalGndMSB, adc2EncalGndLSB, 1) + } +} + +// adc1CalibrationFinish clears ENCAL_GND and reconnects the ADC +// input back to the external pad after self‑calibration. +func (r *regI2C) adc1CalibrationFinish(adcN uint8) { + if adcN == 0 { + r.writeMask(i2cSarADC, i2cSarADCHostID, adc1EncalGndAddr, adc1EncalGndMSB, adc1EncalGndLSB, 0) + } else { + r.writeMask(i2cSarADC, i2cSarADCHostID, adc2EncalGndAddr, adc2EncalGndMSB, adc2EncalGndLSB, 0) + } +} + +// adc1SetCalibrationParam writes the INIT_CODE (offset trim) for +// the selected ADC unit using the regI2C bitfields. +func (r *regI2C) adc1SetCalibrationParam(adcN uint8, param uint32) { + msb := uint8(param >> 8) + lsb := uint8(param & 0xFF) + if adcN == 0 { + r.writeMask(i2cSarADC, i2cSarADCHostID, adc1InitCodeHighAddr, adc1InitCodeHighMSB, adc1InitCodeHighLSB, msb) + r.writeMask(i2cSarADC, i2cSarADCHostID, adc1InitCodeLowAddr, adc1InitCodeLowMSB, adc1InitCodeLowLSB, lsb) + } else { + r.writeMask(i2cSarADC, i2cSarADCHostID, adc2InitCodeHighAddr, adc2InitCodeHighMSB, adc2InitCodeHighLSB, msb) + r.writeMask(i2cSarADC, i2cSarADCHostID, adc2InitCodeLowAddr, adc2InitCodeLowMSB, adc2InitCodeLowLSB, lsb) + } +} + +// waitIdle polls the REGI2C master BUSY bit until it clears or the +// simple software timeout expires. This matches the busy‑wait helper +// used in ESP‑IDF's regi2c_ctrl.c. +func (r *regI2C) waitIdle(reg *volatile.Register32) bool { + for i := 0; i < i2cMstTimeout; i++ { + if reg.Get()&i2cMstBusyBit == 0 { + return true + } + } + return false +} + +// writeMask is a software implementation of REGI2C_WRITE_MASK macro: +// 1. select block + regAddr, +// 2. read current byte, +// 3. update only [msb:lsb] bitfield, +// 4. write it back via internal I2C master. +func (r *regI2C) writeMask(block, hostID, regAddr, msb, lsb, data uint8) { + if hostID != i2cSarADCHostID { + return + } + reg := (*volatile.Register32)(unsafe.Pointer(i2cMstCtrlHost)) + if !r.waitIdle(reg) { + return + } + reg.Set(uint32(block) | uint32(regAddr)<<8) + if !r.waitIdle(reg) { + return + } + cur := (reg.Get() & i2cMstDataMask) >> i2cMstDataShift + mask := uint32(1<<(msb-lsb+1)-1) << lsb + cur &^= mask + cur |= uint32(data&(1<<(msb-lsb+1)-1)) << lsb + reg.Set(uint32(block) | uint32(regAddr)<<8 | i2cMstWrCntl | (cur< 20 { + return errors.New("invalid ADC pin for ESP32-S3") + } + a.Pin.Configure(PinConfig{Mode: PinAnalog}) + InitADC() + + return nil +} + +func (a ADC) Get() uint16 { + if a.Pin < 1 || a.Pin > 20 { + return 0 + } + + var ch uint32 + var raw uint32 + if a.Pin <= 10 { + ch = uint32(a.Pin - 1) // GPIO1→ch0 … GPIO10→ch9 + esp.SENS.SetSAR_MEAS1_MUX_SAR1_DIG_FORCE(0) + esp.SENS.SetSAR_MEAS1_CTRL2_MEAS1_START_FORCE(1) + esp.SENS.SetSAR_MEAS1_CTRL2_SAR1_EN_PAD_FORCE(1) + setSensAtten1(ch, attenDefault) + esp.SENS.SetSAR_MEAS1_CTRL2_SAR1_EN_PAD(1 << ch) + for esp.SENS.GetSAR_SLAVE_ADDR1_SAR_SARADC_MEAS_STATUS() != 0 { + } + esp.SENS.SetSAR_MEAS1_CTRL2_MEAS1_START_SAR(0) + esp.SENS.SetSAR_MEAS1_CTRL2_MEAS1_START_SAR(1) + for esp.SENS.GetSAR_MEAS1_CTRL2_MEAS1_DONE_SAR() == 0 { + } + raw = esp.SENS.GetSAR_MEAS1_CTRL2_MEAS1_DATA_SAR() + } else { + ch = uint32(a.Pin - 11) // GPIO11→ch0 … GPIO20→ch9 + // SENS.SAR_MEAS2_CTRL2: force SW control, select channel + esp.SENS.SetSAR_MEAS2_CTRL2_MEAS2_START_FORCE(1) + esp.SENS.SetSAR_MEAS2_CTRL2_SAR2_EN_PAD_FORCE(1) + esp.SENS.SetSAR_MEAS2_CTRL2_SAR2_EN_PAD(1 << ch) + setSensAtten2(ch, attenDefault) + // APB_SARADC.ARB_CTRL: grant ADC2 to APB for oneshot + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_APB_FORCE(1) + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_GRANT_FORCE(1) + // SENS.SAR_MEAS2_CTRL2.MEAS2_START_SAR: one-shot start + esp.SENS.SetSAR_MEAS2_CTRL2_MEAS2_START_SAR(0) + esp.SENS.SetSAR_MEAS2_CTRL2_MEAS2_START_SAR(1) + for esp.SENS.GetSAR_MEAS2_CTRL2_MEAS2_DONE_SAR() == 0 { + } + raw = esp.SENS.GetSAR_MEAS2_CTRL2_MEAS2_DATA_SAR() + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_APB_FORCE(0) + esp.APB_SARADC.SetARB_CTRL_ADC_ARB_GRANT_FORCE(0) + } + + return uint16(raw&0xfff) << 4 +} + +func (a ADC) GetVoltage() (raw uint32, v float64) { + const samples = 4 + var sum uint32 + for i := 0; i < samples; i++ { + sum += uint32(a.Get()) + } + raw = sum / samples + + // Default full-scale for 11 dB is approximately 3.3 V assuming + // Vref ≈ 1.1 V and gain ≈ 3. If eFuse provided a per-chip DIGI_REF + // (Vref in mV) via adcCalibration, use it to adjust the + // full-scale range instead. + scale := 3.3 + if adcDigiRefMv != 0 { + scale = 3.0 * float64(adcDigiRefMv) / 1000.0 + } + + v = float64(raw) / 65520.0 * scale + return raw, v +} + +// ADC hardware self-calibration for ESP32-S3. +// +// Mapping to ESP-IDF (adc_hal_common.c, hal/esp32s3/adc_ll.h): +// - adc_hal_self_calibration() → ADCSelfCalibrate() +// - adc_ll_calibration_init() → regI2C.ADC1CalibrationInit (DREF=4); +// in IDF it is not called from self_cal, we call it explicitly. +// - adc_ll_calibration_prepare() → SarEnable + ADC1CalibrationPrepare (ENCAL_GND=1) +// - adc_ll_calibration_finish() → ADC1CalibrationFinish (ENCAL_GND=0) +// - adc_ll_set_calibration_param() → ADC1SetCalibrationParam() +// - read_cal_channel() → adcCalibration.readADC1(): +// wait for meas_status==0, start 0→1, wait done, read data +// (similar to adc_oneshot_ll_start + get_raw_result). +// - Loop: 10 iterations, code 0..4096, binary search on self_cal==0; drop min/max; +// rounding (remainder%8 < 4 without +1, otherwise +1) — same as in adc_hal_common.c. +// - raw_check_valid: for ADC1 in IDF always true — we do not check it. +// +// Differences: +// - regI2C: not ROM helper but direct access to 0x6000E000 (protocol like I2C_RTC_CONFIG2). +// - cal_setup: same SENS/atten/controller fields, but through our registers. +// - Result is stored only in hardware for the current session (not in eFuse). +// - eFuse V1: init_code and digi_ref are taken from eFuse — same idea as Arduino/IDF. + +const ( + adcCalTimes = 10 + adcCalOffsetMax = uint32(4096) + adcCalRtcMagic = uint32(0xADC1C401) + adcCalInitMin = uint32(2000) + adcCalInitMax = uint32(3900) + adcDigiRefMinMv = uint32(920) + adcDigiRefMaxMv = uint32(1150) +) + +// adcCalibration encapsulates the self-calibration flow for ADC1 +// and remembers per-chip calibration data (such as DIGI_REF) when it is +// available from eFuse. +type adcCalibration struct { + digiRefMv uint32 +} + +func (c *adcCalibration) calibrate() { + reg := regI2C{} + f := fuse{} + + if vref, ok := f.adc1DigiRefAtten3(); ok { + c.digiRefMv = vref + } + + if saved, ok := c.restoreFromRTC(); ok { + reg.sarEnable() + reg.adc1CalibrationInit(0) + c.adc1CalibrateHigh(reg, saved) + return + } + + initCode, useEfuse := f.adc1InitCodeAtten3() + c.adc1CalibrationSetup(reg) + + if useEfuse { + c.saveToRTC(initCode) + c.adc1CalibrateHigh(reg, initCode) + return + } + + finalCode := c.adc1CalibrateLow(reg) + c.saveToRTC(finalCode) + c.adc1CalibrateHigh(reg, finalCode) +} + +func (c *adcCalibration) getDigiRef() uint32 { + return c.digiRefMv +} + +func (c *adcCalibration) adc1CalibrationSetup(reg regI2C) { + reg.sarEnable() + + esp.SENS.SetSAR_MEAS1_MUX_SAR1_DIG_FORCE(0) + esp.SENS.SetSAR_MEAS1_CTRL2_MEAS1_START_FORCE(0) + esp.SENS.SetSAR_MEAS2_CTRL2_MEAS2_START_FORCE(0) + esp.SENS.SetSAR_MEAS1_CTRL2_SAR1_EN_PAD(0) + setSensAtten1(0, attenDefault) + esp.SENS.SetSAR_MEAS1_CTRL2_MEAS1_START_FORCE(1) + esp.SENS.SetSAR_MEAS1_CTRL2_SAR1_EN_PAD_FORCE(1) + + reg.adc1CalibrationInit(0) + reg.adc1CalibrationPrepare(0) +} + +func (c *adcCalibration) adc1CalibrateLow(reg regI2C) uint32 { + var codeList [adcCalTimes]uint32 + var codeSum uint32 + + for rpt := 0; rpt < adcCalTimes; rpt++ { + codeH := adcCalOffsetMax + codeL := uint32(0) + chkCode := (codeH + codeL) / 2 + reg.adc1SetCalibrationParam(0, chkCode) + selfCal := c.readADC1() + + for codeH-codeL > 1 { + if selfCal == 0 { + codeH = chkCode + } else { + codeL = chkCode + } + chkCode = (codeH + codeL) / 2 + reg.adc1SetCalibrationParam(0, chkCode) + selfCal = c.readADC1() + if codeH-codeL == 1 { + chkCode++ + reg.adc1SetCalibrationParam(0, chkCode) + selfCal = c.readADC1() + } + } + codeList[rpt] = chkCode + codeSum += chkCode + } + + codeL := codeList[0] + codeH := codeList[0] + for i := 0; i < adcCalTimes; i++ { + if codeList[i] < codeL { + codeL = codeList[i] + } + if codeList[i] > codeH { + codeH = codeList[i] + } + } + excluded := codeH + codeL + remaining := codeSum - excluded + finalCode := remaining / (adcCalTimes - 2) + if remaining%(adcCalTimes-2) >= 4 { + finalCode++ + } + + return finalCode +} + +func (c *adcCalibration) adc1CalibrateHigh(reg regI2C, code uint32) { + reg.adc1SetCalibrationParam(0, code) + reg.adc1CalibrationFinish(0) + c.adc1StartWithPadForce() +} + +func (c *adcCalibration) adc1StartWithPadForce() { + esp.SENS.SetSAR_MEAS1_CTRL2_SAR1_EN_PAD_FORCE(1) + esp.SENS.SetSAR_MEAS1_CTRL2_MEAS1_START_FORCE(1) +} + +// readADC1 performs one ADC1 conversion via RTC path (used during calibration). +// Internal GND is connected via ENCAL_GND, so the pin input is disconnected. +// Matches IDF: wait conversion idle (meas_status==0), then start 0→1, wait done, read data. +func (c *adcCalibration) readADC1() uint32 { + for esp.SENS.GetSAR_SLAVE_ADDR1_SAR_SARADC_MEAS_STATUS() != 0 { + } + esp.SENS.SetSAR_MEAS1_CTRL2_MEAS1_START_SAR(0) + esp.SENS.SetSAR_MEAS1_CTRL2_MEAS1_START_SAR(1) + for esp.SENS.GetSAR_MEAS1_CTRL2_MEAS1_DONE_SAR() == 0 { + } + return uint32(esp.SENS.GetSAR_MEAS1_CTRL2_MEAS1_DATA_SAR() & 0xfff) +} + +func (c *adcCalibration) restoreFromRTC() (uint32, bool) { + if esp.RTC_CNTL.GetSTORE0() != adcCalRtcMagic { + return 0, false + } + code := esp.RTC_CNTL.GetSTORE1() + if code < adcCalInitMin || code > adcCalInitMax { + return 0, false + } + return code, true +} + +func (c *adcCalibration) saveToRTC(code uint32) { + esp.RTC_CNTL.SetSTORE0(adcCalRtcMagic) + esp.RTC_CNTL.SetSTORE1(code) +} + +// regI2C — internal I2C for SAR ADC (ESP32-S2 I2C_RTC_CONFIG2, reg 0x6000E000). +// Source: idf-source/components/soc/esp32s3/include/soc/regi2c_saradc.h + +const ( + // I2C_SAR_ADC / I2C_SAR_ADC_HOSTID in regi2c_saradc.h + i2cSarADC = uint8(0x69) // I2C_SAR_ADC + i2cSarADCHostID = uint8(1) // I2C_SAR_ADC_HOSTID + + // ADC_SAR1_DREF_ADDR(_MSB/_LSB) + adc1DrefAddr = uint8(0x2) // ADC_SAR1_DREF_ADDR + adc1DrefMSB = uint8(6) // ADC_SAR1_DREF_ADDR_MSB + adc1DrefLSB = uint8(4) // ADC_SAR1_DREF_ADDR_LSB + + // ADC_SAR2_DREF_ADDR(_MSB/_LSB) + adc2DrefAddr = uint8(0x5) // ADC_SAR2_DREF_ADDR + adc2DrefMSB = uint8(6) // ADC_SAR2_DREF_ADDR_MSB + adc2DrefLSB = uint8(4) // ADC_SAR2_DREF_ADDR_LSB + + // ADC_SAR1_ENCAL_GND_ADDR(_MSB/_LSB) + adc1EncalGndAddr = uint8(0x7) // ADC_SAR1_ENCAL_GND_ADDR + adc1EncalGndMSB = uint8(5) // ADC_SAR1_ENCAL_GND_ADDR_MSB + adc1EncalGndLSB = uint8(5) // ADC_SAR1_ENCAL_GND_ADDR_LSB + + // ADC_SAR2_ENCAL_GND_ADDR(_MSB/_LSB) + adc2EncalGndAddr = uint8(0x7) // ADC_SAR2_ENCAL_GND_ADDR + adc2EncalGndMSB = uint8(7) // ADC_SAR2_ENCAL_GND_ADDR_MSB + adc2EncalGndLSB = uint8(7) // ADC_SAR2_ENCAL_GND_ADDR_LSB + + // ADC_SAR1_INITIAL_CODE_HIGH/LOW_ADDR(_MSB/_LSB) + adc1InitCodeHighAddr = uint8(0x1) // ADC_SAR1_INITIAL_CODE_HIGH_ADDR + adc1InitCodeHighMSB = uint8(3) // ADC_SAR1_INITIAL_CODE_HIGH_ADDR_MSB + adc1InitCodeHighLSB = uint8(0) // ADC_SAR1_INITIAL_CODE_HIGH_ADDR_LSB + adc1InitCodeLowAddr = uint8(0x0) // ADC_SAR1_INITIAL_CODE_LOW_ADDR + adc1InitCodeLowMSB = uint8(7) // ADC_SAR1_INITIAL_CODE_LOW_ADDR_MSB + adc1InitCodeLowLSB = uint8(0) // ADC_SAR1_INITIAL_CODE_LOW_ADDR_LSB + + // ADC_SAR2_INITIAL_CODE_HIGH/LOW_ADDR(_MSB/_LSB) + adc2InitCodeHighAddr = uint8(0x4) // ADC_SAR2_INITIAL_CODE_HIGH_ADDR + adc2InitCodeHighMSB = uint8(3) // ADC_SAR2_INITIAL_CODE_HIGH_ADDR_MSB + adc2InitCodeHighLSB = uint8(0) // ADC_SAR2_INITIAL_CODE_HIGH_ADDR_LSB + adc2InitCodeLowAddr = uint8(0x3) // ADC_SAR2_INITIAL_CODE_LOW_ADDR + adc2InitCodeLowMSB = uint8(7) // ADC_SAR2_INITIAL_CODE_LOW_ADDR_MSB + adc2InitCodeLowLSB = uint8(0) // ADC_SAR2_INITIAL_CODE_LOW_ADDR_LSB + + // Analog config registers for regI2C block (RTC/ANA config in TRM). + anaConfigReg = uintptr(0x6000E044) + i2cSarEnMask = uint32(1 << 18) + anaConfig2Reg = uintptr(0x6000E048) + anaSarCfg2En = uint32(1 << 16) + + // REGI2C master control register and helper masks. + i2cMstCtrlHost1 = uintptr(0x6000E000) + i2cMstBusyBit = uint32(1 << 25) + i2cMstWrCntlBit = uint32(1 << 24) + i2cMstDataMask = uint32(0xFF << 16) + i2cMstDataShift = 16 + i2cMstBusyTimeout = 10000 +) + +type regI2C struct{} + +// waitIdle mimics the IDF regi2c busy-wait helper (see regi2c_ctrl.c). +// It polls the REGI2C master control register until the BUSY bit clears +// or a small timeout expires, to avoid writing while a previous transfer +// is still in progress. +func (r *regI2C) waitIdle(reg *volatile.Register32) bool { + for i := 0; i < i2cMstBusyTimeout; i++ { + if reg.Get()&i2cMstBusyBit == 0 { + return true + } + } + return false +} + +// writeMask is a software implementation of the REGI2C_WRITE_MASK macro +// from IDF (see soc/regi2c_saradc.h). It: +// - selects the regI2C SAR ADC block + register address, +// - reads the current byte, +// - updates only the [msb:lsb] bitfield, +// - writes the new value back via the internal I2C master. +func (r *regI2C) writeMask(block, hostID, regAddr, msb, lsb, data uint8) { + if hostID != i2cSarADCHostID { + return + } + reg := (*volatile.Register32)(unsafe.Pointer(i2cMstCtrlHost1)) + if !r.waitIdle(reg) { + return + } + reg.Set(uint32(block) | uint32(regAddr)<<8) + if !r.waitIdle(reg) { + return + } + cur := (reg.Get() & i2cMstDataMask) >> i2cMstDataShift + mask := uint32(1<<(msb-lsb+1)-1) << lsb + cur &^= mask + cur |= uint32(data&(1<<(msb-lsb+1)-1)) << lsb + reg.Set(uint32(block) | uint32(regAddr)<<8 | i2cMstWrCntlBit | (cur<> 8) + lsb := uint8(param & 0xFF) + if adcN == 0 { + r.writeMask(i2cSarADC, i2cSarADCHostID, adc1InitCodeHighAddr, adc1InitCodeHighMSB, adc1InitCodeHighLSB, msb) + r.writeMask(i2cSarADC, i2cSarADCHostID, adc1InitCodeLowAddr, adc1InitCodeLowMSB, adc1InitCodeLowLSB, lsb) + } else { + r.writeMask(i2cSarADC, i2cSarADCHostID, adc2InitCodeHighAddr, adc2InitCodeHighMSB, adc2InitCodeHighLSB, msb) + r.writeMask(i2cSarADC, i2cSarADCHostID, adc2InitCodeLowAddr, adc2InitCodeLowMSB, adc2InitCodeLowLSB, lsb) + } +} + +// fuse +const ( + // Base address for eFuse controller (EFUSE_BLKx region in TRM). + efuseBase = uintptr(0x60007000) + + // EFUSE_*_REG offsets mirror ESP-IDF's efuse_reg.h layout. + efuseClkReg = efuseBase + 0x1c8 + efuseConfReg = efuseBase + 0x1cc + efuseCmdReg = efuseBase + 0x1d4 + efuseDacConfReg = efuseBase + 0x1e8 + efuseWrTimConf1Reg = efuseBase + 0x1f4 + efuseWrTimConf2Reg = efuseBase + 0x1f8 + efuseRdData4Reg = efuseBase + 0x6c // EFUSE_RD_WR_DIS_REG / RD_DATA4 + efuseRdData5Reg = efuseBase + 0x70 // EFUSE_RD_REPEAT_DATA1_REG / RD_DATA5 + efuseRdData7Reg = efuseBase + 0x78 // EFUSE_RD_REPEAT_DATA3_REG / RD_DATA7 + + // Read opcode and clock enable bit used by EFUSE HAL (see efuse_ll). + efuseReadOpCode = uint32(0x5AA5) + efuseClkEnBit = uint32(1 << 16) + efuseBlkVersionV1 = 1 // EFUSE_BLK_VERSION major version = 1 + + // SYSTEM_PERIP_CLK_EN0 register and EFUSE clock gate bit. + systemPeripClkEn0 = uintptr(0x600C0018) + systemEfuseClkEnBit = uint32(1 << 14) +) + +type fuse struct{} + +// adc1InitCodeAtten3 extracts the ADC1 INIT_CODE (offset trim) for +// attenuation index 3 (typically 11 dB) from EFUSE_BLK2. This mirrors +// the logic used by ESP-IDF's ADC calibration HAL for ESP32-S3. +// +// The code is built from four differential eFuse fields (diff0..diff3) +// and constant offsets (1850, 90, 70) as described in Espressif's +// internal calibration formulas. +func (f *fuse) adc1InitCodeAtten3() (uint32, bool) { + for try := 0; try < 2; try++ { + f.triggerReadSequence() + data4, data5, blkVer := f.readBlock2Data4Data5() + if blkVer != efuseBlkVersionV1 { + continue + } + diff0 := (data4 >> 21) & 0xFF + diff1 := (data4 >> 29) | ((data5 & 7) << 3) + diff2 := (data5 >> 3) & 0x3F + diff3 := (data5 >> 9) & 0x3F + icode0 := diff0 + 1850 + icode1 := diff1 + icode0 + 90 + icode2 := diff2 + icode1 + icode3 := diff3 + icode2 + 70 + if icode3 >= adcCalInitMin && icode3 <= adcCalInitMax { + return icode3, true + } + } + return 0, false +} + +// adc1DigiRefAtten3 reads the digital reference (DIGI_REF) for +// ADC1 at attenuation index 3 from EFUSE_BLK2 / RD_DATA7. This is +// similar to what the ESP-IDF ADC calibration HAL uses when present. +func (f *fuse) adc1DigiRefAtten3() (uint32, bool) { + f.triggerReadSequence() + _, _, blkVer := f.readBlock2Data4Data5() + if blkVer != efuseBlkVersionV1 { + return 0, false + } + data7 := f.readBlock2Data7() + diff3 := (data7 >> 1) & 0xFF + digiRef := diff3 + 900 + if digiRef < adcDigiRefMinMv || digiRef > adcDigiRefMaxMv { + return 0, false + } + return digiRef, true +} + +// triggerReadSequence performs one eFuse read operation using the +// controller's timing/opcode sequence. This roughly corresponds to +// the low-level logic in the ESP-IDF eFuse HAL (see efuse_ll_* in +// the IDF sources and the "eFuse Manager" docs: +// https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/system/efuse.html). +func (f *fuse) triggerReadSequence() { + clk := (*volatile.Register32)(unsafe.Pointer(systemPeripClkEn0)) + clk.Set(clk.Get() | systemEfuseClkEnBit) + efuseClk := (*volatile.Register32)(unsafe.Pointer(efuseClkReg)) + efuseClk.Set(efuseClk.Get() | efuseClkEnBit) + dac := (*volatile.Register32)(unsafe.Pointer(efuseDacConfReg)) + dac.Set(0x28 | (0xFF << 9)) + (*volatile.Register32)(unsafe.Pointer(efuseWrTimConf1Reg)).Set(0x3000 << 8) + (*volatile.Register32)(unsafe.Pointer(efuseWrTimConf2Reg)).Set(0x190) + (*volatile.Register32)(unsafe.Pointer(efuseConfReg)).Set(efuseReadOpCode) + cmd := (*volatile.Register32)(unsafe.Pointer(efuseCmdReg)) + cmd.Set(1) + for cmd.Get()&1 != 0 { + } +} + +// readBlock2Data4Data5 reads the EFUSE_BLK2 data words that contain +// ADC calibration and version information. It returns RD_DATA4, +// RD_DATA5 and the decoded block version (BLK_VERSION). +// +// Layout is derived from the ESP32-S3 TRM and IDF eFuse tables. +func (f *fuse) readBlock2Data4Data5() (data4, data5 uint32, blkVer uint8) { + data4 = (*volatile.Register32)(unsafe.Pointer(efuseRdData4Reg)).Get() + data5 = (*volatile.Register32)(unsafe.Pointer(efuseRdData5Reg)).Get() + blkVer = uint8(data4 & 3) + return data4, data5, blkVer +} + +// readBlock2Data7 reads RD_DATA7 from EFUSE_BLK2, which for ADC +// calibration contains additional reference (DIGI_REF) data fields. +func (f *fuse) readBlock2Data7() uint32 { + return (*volatile.Register32)(unsafe.Pointer(efuseRdData7Reg)).Get() +} + +// readAdcCalibBlock2 triggers an eFuse read and returns the raw +// EFUSE_BLK2 words used for ADC calibration (RD_DATA4/5) along +// with the decoded block version. This is a small helper similar +// in spirit to the internal IDF helpers around EFUSE_BLK2. +func (f *fuse) readAdcCalibBlock2() (data4, data5 uint32, blkVer uint8) { + f.triggerReadSequence() + return f.readBlock2Data4Data5() +}