machine/esp32: add ADC driver (#5595)

* machine/esp32: add ADC driver

Implements ADC1 on the Xtensa ESP32: InitADC, Configure and Get for
GPIO36, GPIO37, GPIO38, GPIO39, GPIO32, GPIO33, GPIO34 and GPIO35
(channels 0-7). The ADC pins are not contiguous on this chip, so the
pin to channel mapping is a lookup rather than arithmetic as on the
ESP32-S3.

Conversions are driven by the RTC controller under software control,
and Get returns the 12-bit sample scaled to 0..65520 to match the
other ESP ADC drivers.

The analog pads are spread over three unrelated RTC_IO registers
(SENSOR_PADS, XTAL_32K_PAD and ADC_PAD), so pad setup is kept local to
this file rather than adding a PinAnalog mode to machine_esp32.go.
That keeps the change to a single file.

Values are raw and uncalibrated. Unlike the ESP32-C3, S3 and C6
drivers there is no eFuse or self-calibration step; accurate voltage
mapping should be done with a two-point calibration in user code.

ADC2 is not implemented. On the ESP32 it is shared with the Wi-Fi
radio and cannot be used reliably while the radio is active.

Tested on an ESP32 Coreboard V2 with a photoresistor divider on
GPIO36. Readings swept 5056..59824 over the light range, all eight
channels returned independent values, and an invalid pin returned an
error from Configure and 0 from Get.

Signed-off-by: zombieleet <osikwemhev@gmail.com>

* fix: use package level err definiition and return (uint32, bool) from adcRTCGPIO

---------

Signed-off-by: zombieleet <osikwemhev@gmail.com>
This commit is contained in:
V
2026-08-25 13:24:05 +02:00
committed by GitHub
parent 242f34d71a
commit 840af7a842
2 changed files with 239 additions and 0 deletions
+2
View File
@@ -989,6 +989,8 @@ endif
ifneq ($(XTENSA), 0) ifneq ($(XTENSA), 0)
$(TINYGO) build -size short -o test.bin -target=esp32-generic examples/machinetest $(TINYGO) build -size short -o test.bin -target=esp32-generic examples/machinetest
@$(MD5SUM) test.bin @$(MD5SUM) test.bin
$(TINYGO) build -size short -o test.bin -target=esp32-coreboard-v2 examples/adc
@$(MD5SUM) test.bin
$(TINYGO) build -size short -o test.bin -target=esp32c3-generic examples/machinetest $(TINYGO) build -size short -o test.bin -target=esp32c3-generic examples/machinetest
@$(MD5SUM) test.bin @$(MD5SUM) test.bin
$(TINYGO) build -size short -o test.bin -target=esp32s3-generic examples/machinetest $(TINYGO) build -size short -o test.bin -target=esp32s3-generic examples/machinetest
+237
View File
@@ -0,0 +1,237 @@
//go:build esp32
// ESP32 (Xtensa): SAR ADC1, 12-bit hardware; Get() returns 0..65520 (scaled
// from 12-bit).
//
// Pin mapping is not contiguous, so a lookup is used instead of arithmetic:
// ADC1 channels 0..7 are GPIO36, 37, 38, 39, 32, 33, 34, 35.
//
// ADC1 is driven by the RTC controller under software control: SAR1_DIG_FORCE
// selects the RTC controller, MEAS1_START_FORCE and SAR1_EN_PAD_FORCE hand
// channel selection and triggering to software, then MEAS1_START_SAR 0->1
// starts a conversion and MEAS1_DATA_SAR holds the 12-bit result.
//
// Get() returns raw, uncalibrated values. Unlike the ESP32-C3/S3/C6 drivers
// there is no eFuse or self-calibration step here; accurate voltage mapping
// should be done with a two-point calibration in user code.
//
// ADC2 is deliberately not implemented. On the ESP32 it is shared with the
// Wi-Fi radio and cannot be used reliably while the radio is active.
package machine
import (
"device/esp"
"errors"
)
const (
// 11 dB, the widest input range the SAR offers (IDF ADC_ATTEN_DB_12).
adcAtten11dB = 3
// SAR1_BIT_WIDTH / SAR1_SAMPLE_BIT value selecting 12-bit conversions.
adcWidth12Bit = 3
// Divider for the ADC's own clock, matching the IDF default for ADC1.
adcClkDiv = 2
)
var (
ErrInvalidADCPin = errors.New("invalid ADC pin for ESP32")
)
// InitADC powers up SAR ADC1 and puts it under software control.
func InitADC() {
// The SAR front end is shared with the hall sensor and an internal
// amplifier. Both must be off or channel readings pick up their bias.
esp.SENS.SetSAR_TOUCH_CTRL1_XPD_HALL_FORCE(1)
esp.SENS.SetSAR_TOUCH_CTRL1_HALL_PHASE_FORCE(1)
esp.RTC_IO.SetHALL_SENS_XPD_HALL(0)
esp.SENS.SetSAR_MEAS_WAIT2_FORCE_XPD_AMP(2) // 2 = force power down
esp.SENS.SetSAR_MEAS_CTRL_AMP_RST_FB_FSM(0)
esp.SENS.SetSAR_MEAS_CTRL_AMP_SHORT_REF_FSM(0)
esp.SENS.SetSAR_MEAS_CTRL_AMP_SHORT_REF_GND_FSM(0)
esp.SENS.SetSAR_MEAS_WAIT1_SAR_AMP_WAIT1(1)
esp.SENS.SetSAR_MEAS_WAIT1_SAR_AMP_WAIT2(1)
esp.SENS.SetSAR_MEAS_WAIT2_SAR_AMP_WAIT3(1)
// Drive ADC1 from the RTC controller rather than the digital/DMA path.
// The RTC domain is always clocked, so no clock gate has to be opened.
esp.SENS.SetSAR_READ_CTRL_SAR1_DIG_FORCE(0)
esp.SENS.SetSAR_MEAS_START1_MEAS1_START_FORCE(1)
esp.SENS.SetSAR_MEAS_START1_SAR1_EN_PAD_FORCE(1)
esp.SENS.SetSAR_MEAS_WAIT2_FORCE_XPD_SAR(3) // 3 = force power up
esp.SENS.SetSAR_START_FORCE_SAR1_BIT_WIDTH(adcWidth12Bit)
esp.SENS.SetSAR_READ_CTRL_SAR1_SAMPLE_BIT(adcWidth12Bit)
esp.SENS.SetSAR_READ_CTRL_SAR1_CLK_DIV(adcClkDiv)
// ADC1 returns the sample inverted; this flips it so readings rise with
// the input voltage.
esp.SENS.SetSAR_READ_CTRL_SAR1_DATA_INV(1)
}
// Configure routes the pin to the SAR ADC and sets its attenuation. It returns
// an error if the pin has no ADC1 channel. ADCConfig is accepted for API
// compatibility but its fields are not used; attenuation is fixed at 11 dB.
func (a ADC) Configure(config ADCConfig) error {
ch, ok := adc1Channel(a.Pin)
if !ok {
return ErrInvalidADCPin
}
configureADCPad(a.Pin)
setSensAtten1(ch, adcAtten11dB)
return nil
}
// Get runs a single conversion and returns the result scaled from the 12-bit
// hardware value to the full 16-bit range, so values run 0..65520. It returns
// 0 if the pin has no ADC1 channel.
func (a ADC) Get() uint16 {
ch, ok := adc1Channel(a.Pin)
if !ok {
return 0
}
// SAR1_EN_PAD is a one-hot mask, not a channel index.
esp.SENS.SetSAR_MEAS_START1_SAR1_EN_PAD(1 << ch)
// The touch and ULP state machines share the SAR over an internal bus;
// wait for a conversion already in flight to finish.
for esp.SENS.GetSAR_SLAVE_ADDR1_MEAS_STATUS() != 0 {
}
// The conversion is triggered by the 0->1 edge, so the bit has to be
// taken low first in case a previous call left it high.
esp.SENS.SetSAR_MEAS_START1_MEAS1_START_SAR(0)
esp.SENS.SetSAR_MEAS_START1_MEAS1_START_SAR(1)
for esp.SENS.GetSAR_MEAS_START1_MEAS1_DONE_SAR() == 0 {
}
raw := esp.SENS.GetSAR_MEAS_START1_MEAS1_DATA_SAR()
return uint16(raw&0xfff) << 4
}
// adc1Channel maps a pin to its ADC1 channel. The ESP32's ADC pins are not
// contiguous, so this cannot be computed from the pin number.
func adc1Channel(p Pin) (uint32, bool) {
switch p {
case GPIO36:
return 0, true
case GPIO37:
return 1, true
case GPIO38:
return 2, true
case GPIO39:
return 3, true
case GPIO32:
return 4, true
case GPIO33:
return 5, true
case GPIO34:
return 6, true
case GPIO35:
return 7, true
}
return 0, false
}
// configureADCPad hands the pad to the RTC domain and takes it out of digital
// mode, so the SAR sees the analog level.
//
// The pads are spread over three different RTC_IO registers with unrelated
// field names, so each group is handled separately. GPIO34-39 have no internal
// pull resistors, which is why only the GPIO32/33 group disables them.
func configureADCPad(p Pin) {
switch p {
case GPIO36:
esp.RTC_IO.SetSENSOR_PADS_SENSE1_MUX_SEL(1)
esp.RTC_IO.SetSENSOR_PADS_SENSE1_FUN_SEL(0)
esp.RTC_IO.SetSENSOR_PADS_SENSE1_FUN_IE(0)
case GPIO37:
esp.RTC_IO.SetSENSOR_PADS_SENSE2_MUX_SEL(1)
esp.RTC_IO.SetSENSOR_PADS_SENSE2_FUN_SEL(0)
esp.RTC_IO.SetSENSOR_PADS_SENSE2_FUN_IE(0)
case GPIO38:
esp.RTC_IO.SetSENSOR_PADS_SENSE3_MUX_SEL(1)
esp.RTC_IO.SetSENSOR_PADS_SENSE3_FUN_SEL(0)
esp.RTC_IO.SetSENSOR_PADS_SENSE3_FUN_IE(0)
case GPIO39:
esp.RTC_IO.SetSENSOR_PADS_SENSE4_MUX_SEL(1)
esp.RTC_IO.SetSENSOR_PADS_SENSE4_FUN_SEL(0)
esp.RTC_IO.SetSENSOR_PADS_SENSE4_FUN_IE(0)
case GPIO32:
esp.RTC_IO.SetXTAL_32K_PAD_X32P_MUX_SEL(1)
esp.RTC_IO.SetXTAL_32K_PAD_X32P_FUN_SEL(0)
esp.RTC_IO.SetXTAL_32K_PAD_X32P_FUN_IE(0)
esp.RTC_IO.SetXTAL_32K_PAD_X32P_RUE(0)
esp.RTC_IO.SetXTAL_32K_PAD_X32P_RDE(0)
case GPIO33:
esp.RTC_IO.SetXTAL_32K_PAD_X32N_MUX_SEL(1)
esp.RTC_IO.SetXTAL_32K_PAD_X32N_FUN_SEL(0)
esp.RTC_IO.SetXTAL_32K_PAD_X32N_FUN_IE(0)
esp.RTC_IO.SetXTAL_32K_PAD_X32N_RUE(0)
esp.RTC_IO.SetXTAL_32K_PAD_X32N_RDE(0)
case GPIO34:
// ADC_PAD_ADC1 is a pad name: it is GPIO34, ADC1 channel 6.
esp.RTC_IO.SetADC_PAD_ADC1_MUX_SEL(1)
esp.RTC_IO.SetADC_PAD_ADC1_FUN_SEL(0)
esp.RTC_IO.SetADC_PAD_ADC1_FUN_IE(0)
case GPIO35:
// ADC_PAD_ADC2 is a pad name: it is GPIO35, ADC1 channel 7.
esp.RTC_IO.SetADC_PAD_ADC2_MUX_SEL(1)
esp.RTC_IO.SetADC_PAD_ADC2_FUN_SEL(0)
esp.RTC_IO.SetADC_PAD_ADC2_FUN_IE(0)
default:
return
}
ch, ok := adcRTCGPIO(p)
if !ok {
return
}
// Take the RTC output driver off the pad so nothing fights the input.
esp.RTC_IO.SetENABLE_W1TC(1 << ch)
}
// adcRTCGPIO maps a pin to its index within the RTC GPIO block, which is
// numbered independently of the main GPIO matrix.
func adcRTCGPIO(p Pin) (uint32, bool) {
switch p {
case GPIO36:
return 0, true
case GPIO37:
return 1, true
case GPIO38:
return 2, true
case GPIO39:
return 3, true
case GPIO34:
return 4, true
case GPIO35:
return 5, true
case GPIO33:
return 8, true
case GPIO32:
return 9, true
}
return 0, false
}
// setSensAtten1 sets the 2-bit attenuation field for one ADC1 channel. The
// generated code exposes SAR_ATTEN1 only as a whole register, so the
// read-modify-write is done here.
func setSensAtten1(ch, atten uint32) {
v := esp.SENS.GetSAR_ATTEN1()
v &^= 3 << (ch * 2)
v |= (atten & 3) << (ch * 2)
esp.SENS.SetSAR_ATTEN1(v)
}