Files
tinygo/src/machine/machine_esp32s3_adc.go
T
deadprogram f902437848 esp32c3/esp32s3: refactor ADC implementation to reduce code duplication.
This refactoring reduces code duplication from the esp32c3/esp32s3 ADC
implementation, by reusing the register/efuse calibration code since the
same basic procedures are used by both processors.

Signed-off-by: deadprogram <ron@hybridgroup.com>
2026-04-05 14:15:40 +02:00

433 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//go:build esp32s3
// ESP32-S3: 2 SAR ADCs, 12-bit hardware; Get() returns 0..65520 (scaled from 12-bit).
// Pin mapping: ADC1 = GPIO 1..10 (channel = GPIO-1); ADC2 = GPIO 11..20 (channel = GPIO-11).
// Get() returns raw, uncalibrated ADC values; accurate 03.3V mapping should be done
// either by a two-point calibration in user code or by using the eFuse-based
// calibration logic (see IDF adc_cali / our ADCSelfCalibrate implementation).
//
// Registers used (TRM / IDF):
// SYSTEM: PERIP_RST_EN0.APB_SARADC_RST, PERIP_CLK_EN0.APB_SARADC_CLK_EN
// RTC_CNTL: ANA_CONF.SAR_I2C_PU, I2C_RESET_POR_FORCE_PU
// ADC1 RTC path (oneshot, TRM/IDF):
// SENS.SAR_MEAS1_MUX.SAR1_DIG_FORCE = 0 → ADC1 under RTC (not digital/APB)
// SENS.SAR_MEAS1_CTRL2.MEAS1_START_FORCE = 1, SAR1_EN_PAD_FORCE = 1 → SW triggers and selects channel
// Per conversion: set attenuation (SAR_ATTEN1), channel (SAR1_EN_PAD), then MEAS1_START_SAR 0→1; wait MEAS1_DONE_SAR; read MEAS1_DATA_SAR.
// SENS.SAR_MEAS1_CTRL1: amp/ref (FORCE_XPD_AMP etc). SAR_MEAS1_CTRL2: MEAS1_DONE_SAR (done), MEAS1_START_SAR (start), MEAS1_DATA_SAR (12-bit result).
// APB_SARADC: FSM_WAIT, CLKM, etc. used for clock/shared logic; ADC2 uses ARB_CTRL.
package machine
import (
"device/esp"
"errors"
"runtime/volatile"
"unsafe"
)
// newRegI2C returns the regI2C configured for ESP32-S3: hostID=1, drefInit=4.
func newRegI2C() regI2C { return regI2C{hostID: 1, drefInit: 4} }
var adcDigiRefMv uint32
func InitADC() {
// SYSTEM: reset and enable APB_SARADC clock so SAR registers are accessible.
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)
// SENS.SAR_PERI_CLK_GATE_CONF: enable SENS SAR peripheral clock (matches Arduino/IDF runtime state).
esp.SENS.SetSAR_PERI_CLK_GATE_CONF_SARADC_CLK_EN(1)
// RTC_CNTL.ANA_CONF: keep internal SAR I2C (regI2C analog bus) powered and out of reset.
esp.RTC_CNTL.SetANA_CONF_I2C_RESET_POR_FORCE_PD(0)
esp.RTC_CNTL.SetANA_CONF_SAR_I2C_PU(1)
esp.RTC_CNTL.SetANA_CONF_I2C_RESET_POR_FORCE_PU(1)
// SENS.SAR_POWER: power up SAR analog block and enable SAR internal clock.
esp.SENS.SetSAR_POWER_XPD_SAR_FORCE_XPD_SAR(3)
esp.SENS.SetSAR_POWER_XPD_SAR_SARCLK_EN(1)
// SENS.SAR_MEAS1_CTRL1: force ADC1 front-end amplifier and reference on in RTC oneshot mode.
esp.SENS.SetSAR_MEAS1_CTRL1_FORCE_XPD_AMP(3)
esp.SENS.SetSAR_MEAS1_CTRL1_AMP_RST_FB_FORCE(3)
esp.SENS.SetSAR_MEAS1_CTRL1_AMP_SHORT_REF_FORCE(3)
esp.SENS.SetSAR_MEAS1_CTRL1_AMP_SHORT_REF_GND_FORCE(3)
// SENS.SAR_AMP_CTRL1/2: amplifier/reference settling timings (same as cold-boot defaults).
esp.SENS.SetSAR_AMP_CTRL1_SAR_AMP_WAIT1(10)
esp.SENS.SetSAR_AMP_CTRL1_SAR_AMP_WAIT2(10)
esp.SENS.SetSAR_AMP_CTRL2_SAR_XPD_SAR_AMP_FSM_IDLE(1)
esp.SENS.SetSAR_AMP_CTRL2_SAR_AMP_SHORT_REF_GND_FSM_IDLE(1)
// ADC2 uses the same InitADC() as ADC1 (shared APB_SARADC clock/FSM).
// SENS.SAR_MEAS2_CTRL1: ADC2 FSM wait timings for power-up/reset/standby.
esp.SENS.SetSAR_MEAS2_CTRL1_SAR_SAR2_XPD_WAIT(8)
esp.SENS.SetSAR_MEAS2_CTRL1_SAR_SAR2_RSTB_WAIT(8)
esp.SENS.SetSAR_MEAS2_CTRL1_SAR_SAR2_STANDBY_WAIT(100)
esp.SENS.SetSAR_MEAS2_CTRL1_SAR_SAR2_RSTB_FORCE(3)
// SENS.SAR_MEAS1_MUX / SAR_MEAS1_CTRL2: route ADC1 to RTC controller and use SW to select channel/start.
esp.SENS.SetSAR_MEAS1_MUX_SAR1_DIG_FORCE(0) // 0 = controlled by RTC/SENS, not digital/APB.
esp.SENS.SetSAR_MEAS1_CTRL2_MEAS1_START_FORCE(1) // SW triggers conversion.
esp.SENS.SetSAR_MEAS1_CTRL2_SAR1_EN_PAD_FORCE(1) // SW selects which ADC1 pad is enabled.
// APB_SARADC: shared FSM/clock config used by both ADC units and the ADC2 arbiter.
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.SetCTRL_SARADC_XPD_SAR_FORCE(3)
esp.APB_SARADC.SetCTRL_SARADC_SAR_CLK_GATED(1)
esp.APB_SARADC.SetCTRL2_SARADC_SAR1_INV(0)
esp.APB_SARADC.SetCTRL2_SARADC_SAR2_INV(0)
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)
esp.APB_SARADC.SetFILTER_CTRL1_FILTER_FACTOR0(0)
esp.APB_SARADC.SetFILTER_CTRL1_FILTER_FACTOR1(0)
adcSelfCalibrate()
adcDigiRefMv = getDigiRef()
}
const (
attenDefault = 3 // 11 dB, ~0..3.3 V (IDF ADC_ATTEN_DB_12)
)
func setSensAtten1(ch, atten uint32) {
// SENS.SAR_ATTEN1: 2 bits per channel
v := esp.SENS.GetSAR_ATTEN1()
v &^= 3 << (ch * 2)
v |= (atten & 3) << (ch * 2)
esp.SENS.SetSAR_ATTEN1(v)
}
func setSensAtten2(ch, atten uint32) {
// SENS.SAR_ATTEN2: 2 bits per channel
v := esp.SENS.GetSAR_ATTEN2()
v &^= 3 << (ch * 2)
v |= (atten & 3) << (ch * 2)
esp.SENS.SetSAR_ATTEN2(v)
}
func (a ADC) Configure(config ADCConfig) error {
if a.Pin < 1 || a.Pin > 20 {
return errors.New("invalid ADC pin for ESP32-S3")
}
a.Pin.Configure(PinConfig{Mode: PinAnalog})
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.calibrationInit (DREF=4);
// in IDF it is not called from self_cal, we call it explicitly.
// - adc_ll_calibration_prepare() → SarEnable + calibrationPrepare (ENCAL_GND=1)
// - adc_ll_calibration_finish() → calibrationFinish (ENCAL_GND=0)
// - adc_ll_set_calibration_param() → setCalibrationParam()
// - 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
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.
func adcSelfCalibrate() {
reg := newRegI2C()
f := fuse{}
if vref, ok := f.adc1DigiRefAtten3(); ok {
adcDigiRefMv = vref
}
if saved, ok := restoreFromRTC(); ok {
reg.sarEnable()
reg.calibrationInit(0)
adc1CalibrateHigh(reg, saved)
return
}
initCode, useEfuse := f.adc1InitCodeAtten3()
adc1CalibrationSetup(reg)
if useEfuse {
saveToRTC(initCode)
adc1CalibrateHigh(reg, initCode)
return
}
finalCode := reg.calibrateBinarySearch(0, adcCalTimes, readADC1)
saveToRTC(finalCode)
adc1CalibrateHigh(reg, finalCode)
}
func getDigiRef() uint32 {
return adcDigiRefMv
}
func 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.calibrationInit(0)
reg.calibrationPrepare(0)
}
func adc1CalibrateHigh(reg regI2C, code uint32) {
reg.setCalibrationParam(0, code)
reg.calibrationFinish(0)
adc1StartWithPadForce()
}
func 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 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 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 saveToRTC(code uint32) {
esp.RTC_CNTL.SetSTORE0(adcCalRtcMagic)
esp.RTC_CNTL.SetSTORE1(code)
}
// 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()
}