13 KiB
Analog to Digital Converters
This project lets you control a couple of LEDs with a potentiometer and a capacitive touch sensor, while reading out the values of the Analog to Digital Converter (ADC) from the potentiometer.
The wire coming off pin 14 in the breadboard is just a bare wire hanging off the side for capacitive touch sensing.
Lessons Learned
- How potentiometers actually work (surprise: it's more complicated than I thought, as usual)
- How an ADC works
- How the ESP32-S3 ADC in particular works
- How to build a Flash ADC on a breadboard
- How integrated circuits measure capacitance
Measuring potentiometers
I've had some prior experience working with potentiometers in electric guitars, but never really did much beyond wiring them and comparing the way they colored the tone of the guitar. I knew that they are essentially adjustable resistors, but beyond that, I never really did much with them.
GND LOAD SUPPLY
1 2 3
| | |
The project connects a potentiometer's pin 3 to the 3.3V power rail and the wiper (pin 2) to the ADC input pin (pin 1) of the ESP32-S3. It reads the computed value of the Analog to Digital Converter from that pin. Reading those values out and printing them on the serial UART was really easy, but I wanted to verify the numbers. The voltage measurement should be easy to check with a voltmeter. I assumed that I could check across pins 1 and 3 and, as I adjust the potentiometer, I would see the output voltage on the GND side adjust.
Wrong! The voltage across pins 3 and 1 stays constant as the brush moves along the potentiometer's range. The difference can only be measured across the load and supply pins (1 and 2). This is because the wiper physically moves between the points 1 and 2 which is how the resistance changes, so there will only be a visible difference between the wiper and ground.
That didn't make sense with my previous understanding of potentiometers as "adjustable resistors", because that understanding was incomplete. Potentiometers are adjustable resistors (just like a rheostat), but with 3 pins, they're acting as two resistors in series like a voltage divider. Inside a potentiometer, there's a resistive element that stretches all the way from pin 3 to pin 1. Think of it like a resistor that's been stretched out. The wiper, attached to pin 2, is less like a resistor adjuster, and more like a probe that contacts the resistive material at various points along its length. The further away from 3 and closer to 1 we get, the higher that resistance value gets. The wiper itself isn't actually introducing significant resistance, and is not therefore significantly changing the potential difference between 1 and 3. It's just putting in a tap at the point where the wiper contacts the resistive track.
Now if you connect the load pin to something that is ACTUALLY drawing a load, rather than something like an ADC that really isn't drawing much at all, then current will be pulled from the wiper contact point and the voltage observed at the wiper pin will sag. This isn't really a necessary detail for this project, but it's something to keep in mind for future projects where we might put something like a motor on a potentiometer.
How an ADC works
The ESP32 GPIO pins understand HIGH and LOW. So if we want to say "How wide has the user opened that potentiometer over there?", the GPIO pins can only tell us "fully open" or "fully closed" (keeping in mind that the specific voltage that the GPIO pin understands as HIGH and LOW may or may not directly correspond to the fully open and fully closed position of the potentiometer). To do that, we need to use an Analog to Digital Converter (ADC) to express the analog value as a magnitude between 0 and V, rather than ON and OFF.
An ADC measures the voltage on an input and returns a digital value as an integer having precision P in bits. So if an ADC returns a reading with a precision of 12 bits, the reading can range from 0 (being 0v) to 4095 (being 100% of the possible input voltage). An ADC is a collection of circuits that perform this job as a single package, whether integrated on the chip, or as a separate discrete component. There's a bunch of different ways this can be done, but the general theory is that the ADC is a circuit that takes an input voltage and expresses it as an integer representing a quantity between 0 and 100% of the potential maximum.
One of the simplest ADCs to implement is called a Flash ADC, and that one is simple enough we can build it on a breadboard. I can't build this one yet - I need to come back to this when some components arrive in the mail.
- Need to come back and build a flash ADC when my components get here
How the ESP32-S3 ADC works
I really wanted to understand how the ESP32-S3 in particular accomplishes this. So I went through a deep dive of the reference manual, plus some googling, plus some GPT interpretation of the TRM, and figured some things out.
The ESP32-S3 has two ADCs on the package that both perform in the same basic fashion, but are controlled in slightly different ways. Per the technical reference manual
Two 12-bit Successive Approximation ADCs (SAR ADCs) controlled by five dedicated controllers that can input analog signals from total of 20 channels. The SAR ADCs can operate in a high-performance mode or a low-power mode.
There are two ADCs on the ESP32-S3, being ADC1 and ADC2. These are two distinct hardware units - they are not part of the CPU or the ULP, they are distinct peripherals. They can both be operated in a low power mode (by driving them with the real-time clock controller) or in a high power mode (using the digital controller). ADC2 is shared among many peripherals such as WiFi, and so it has an "arbiter" hardware component that controls who gets to use it at which times and which controller drives it. ADC1 appears to be dedicated to userland programs running on the CPU, so it does not have a separate hardware arbiter, it has a much simpler mux device that selects between the RTC and Digital controller based on a hardware register.
Arduino HAL be Dummy Thicc
As previously mentioned, the HAL is quite thick here. The HAL hides the grand majority of the complexity I'm about to talk about, underneath the simple concepts of oneshot and continuous sampling modes. The source in this project uses the oneshot sampling mechanism to pull ADC values from pin 1 on request:
int adcvalue = analogRead(PIN_ANALOG_IN);
... This engages the RTC controller on ADC1 to pull a single value and return it immediately. There is a second mode of operation, analogContinuous, wherein the ADC is configured to continually sample values, store the results, and call a user callback function when data is ready.
// Optional for ESP32: Set the resolution to 9-12 bits (default is 12 bits)
analogContinuousSetWidth(12);
// Optional: Set different attenaution (default is ADC_11db)
analogContinuousSetAtten(ADC_11db);
// Setup ADC Continuous with following input:
// array of pins, count of the pins, how many conversions per pin in one cycle will happen, sampling frequency, callback function
analogContinuous(adc_pins, adc_pins_count, CONVERSIONS_PER_PIN, 20000, &adcComplete);
// Start ADC Continuous conversions
analogContinuousStart();
The demo code I worked with didn't use the continuous mode, but I can definitely see utility for it.
As usual the Arduino HAL makes this all really easy and hides a bunch of complexity from us. Part of the whole reason I'm doing these projects is as a rejection of abstraction layers between me and the computer, so, I'm not willing to stop there. Let's grab a shovel and dig into what's actually happening.
ADC hardware registers
All of the ADC hardware is controlled by writing values to the ADC hardware registers. Once these are changed, the ADC hardware sees the change and starts acting on the request.
The hardware registers are all memory mapped in to various regions above 0x5000_0000. These memory addresses are memory mapped in from the low power mode peripherals in the address controller, and are part of the shared instruction/data bus. Basically, when something requests data from those memory regions, the memory address controller redirects the request out to the appropriate peripheral on behalf of whoever is requesting that memory. The peripheral in question then manages the requested item, which is usually stored in a hardware register inside the peripheral. Memory mapped registers are a really common way of controlling peripherals and reduces the complexity of connections between components in the package.
There are some other registers that I'm not mentioning here, that allow you to do things like set monitors and thresholds on the ADC when it's being driven by the digital controller, that will fire interrpts when they are exceeded. If you really want to dig deep, grab a copy of the ESP32-S3 technical reference manual.
Driving the ADC : RTC, Digital, and PWDET
There are three ways to drive the ADC : using the real time clock driver, the digital driver, or the PWDET driver. ADC1 can use the digital or RTC driver, selected by the user. ADC2 can use the digital, RTC, or PWDET driver, but the mode is controlled by the arbiter hardware device.
The PWDET driver is the one I know the least about, but I'm sure you can do some nifty stuff with it. Basically it's used only internally to monitor RF power, looks like it's used by the WiFi controller somehow.
RTC ADC operation
Hardware Registers
The ADC RTC hardware registers are mapped into memory between 0x6000_8000 and 0x6000_8FFF.
| ADC | Register | Address | Purpose |
|---|---|---|---|
| ADC1 | SENS_SAR_READER1_CTRL_REG |
0x6000_8000 |
Controlling ADC1 data and sampling |
| ADC1 | SENS_SAR_MEAS1_CTRL2_REG |
0x6000_800C |
Activating ADC1 and reading the output of the operation |
| ADC1 | SENS_SAR_MEAS1_MUX_REG |
0x6000_8010 |
Selecting the RTC or Digital controller for ADC1 operations |
| ADC1 | SENS_SAR_ATTEN1_REG |
0x6000_8014 |
Setting the attenuation for ADC1 |
| ADC2 | SENS_SAR_MEAS2_MUX_REG |
0x6000_8024 |
Selecting the RTC or Digital controller for ADC1 operations |
| ADC2 | SENS_SAR_MEAS2_CTRL2_REG |
0x6000_8030 |
Activating ADC1 and reading the output of the operation |
| ADC2 | SENS_SAR_READER2_CTRL_REG |
0x6000_8034 |
Controlling ADC1 data and sampling |
| ADC2 | SENS_SAR_ATTEN2_REG |
0x6000_8038 |
Setting the attenuation for ADC1 |
| Both | SENS_SAR_POWER_XPD_SAR_REG |
0x6000_803C |
SAR ADC Power Control |
RTC ADC Operation
The RTC driver lives in the low power portion of the package, so that it can still be driven when the CPU and ULP are asleep. (For example, the System RTC can be configured so that every time it fires, the ULP is awoken from sleep, and it performs various operations, including checking the ADC RTC configuration and firing if it is so configured).
The RTC operation looks like this:
- System RTC fires
- Is ULP asleep? Should I wake it?
- If no, do nothing with the ADC
- If yes, ULP is woken up
- ULP issues an ADP command to the ADC RTC
- ADC RTC controller has some logic wired in that uses bits from the
SENS_SAR_MEAS1_CTRL2_REGto determine who can turn it on and when.- When
SENS_MEAS1_START_FORCE = 0, the ULP is allowed to start the ADC RTC Reader. - When the
SENS_SAR1_DIG_FORCEbit ofSENS_SAR_MEAS1_MUX_REGis unset, the output of the ADC RTC Reader is used for the SAR ADC operation
- When
- The ADC performs a SAR operation, sets the value in the low word of
SEND_SAR_MEAS1_CTRL2_REG, and sets theSENS_MEAS1_DONE_SARwhen the operation is complete and the data is ready
- Is ULP asleep? Should I wake it?
Since the RTC ADC controller is designed for one-time conversion operations, historically, the RTC pathway is how the HAL drives the ADC for one-shot analogRead events. But it varies slightly from the way that the default system timer pathway works.
- User calls
analogRead- HAL sets the
SENS_MEAS1_START_FORCEbit ofSENS_SAR_MEAS1_CTRL2_REGto1, which configures the ADC to allow software start requests via theSENS_MEAS1_STARTbit - HAL sets the
SENS_MEAS1_STARTbit ofSENS_SAR_MEAS1_CTRL2_REGto1, which forces the RTC ADC to fire- When the
SENS_SAR1_DIG_FORCEbit ofSENS_SAR_MEAS1_MUX_REGis unset, the output of the ADC RTC Reader is used for the SAR ADC operation
- When the
- The ADC performs a SAR operation, sets the value in the low word of
SEND_SAR_MEAS1_CTRL2_REG, and sets theSENS_MEAS1_DONE_SARwhen the operation is complete and the data is ready
- HAL sets the


