Files
esp32-learning/04-adc/README.md

27 KiB

Analog to Digital Converters

breadboard

This project lets you control a couple of LEDs with a potentiometer and a capacitive touch sensor, using the potentiometer through the analog to digital converter as a dimmer for the green LED vi PWM, and using the capacitive touch sensor as an on/off switch for the red LED.

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'll build one of these at some point and document it, but for right now I want to get through this lesson and move on to the next one. I am drowning in ADC knowledge and must now vomit it out.

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.

hold on to your butts

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.

ESP32-S3 SAR ADC diagram

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 ESP32 hardware peripheral 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.

Driving and Arbitrating the ADC

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.
  • The RTC driver is connected to the system real time clock (using a configurable clock divider) and the ULP.
  • The digital driver is driven either by its own clock (APB_CLK) or by the PLL_240M_CLK using a configurable clock divider and needs the CPU and ULP to be awake.

ADC1 can be driven on demand by choosing either the RTC or the Digital modes of operation, and firing off a sampling event. ADC2, however, is controlled by the ADC2 Arbiter hardware, which acts as a scheduler for the device. It is the job of the arbiter to ensure there are no hardware conflicts accessing the ADC2 resource. The arbiter is aware of the priority of each ADC2 consumer, and has two modes of operation:

  • Fair arbitration. In this mode, cyclic priority arbitration is used. Essentially this means that customer A is at the front of the line; they got served first; they go to the back of the line, and the next customer gets served. This ensures all requestors get access and nobody can monopolize the resource.
    • Fixed priority arbitration. In this mode, the RTC, the digital controller, and the PWDET controller are all given priority values (via APB_SARADC_ADC_ARB_*_PRIORITY registers), and higher priority customers will pre-empt lower priority customers, even if that lower priority customer is in the middle of an operation. When a lower priority customer is pre-empted, bitflags are set on its output data is set to indicate that the sample was either interrupted or not started.

The arbiter can actually be masked off with the GRANT and FORCE flags of the APB_SARADC_APB_ADC_ARB_CTRL_REG register, to completely take arbitration logic of the loop, using ADC2 in a similar way to how ADC1 is used. I have to imagine that doing so will make your ESP32-S3's wifi .... very unhappy.

RTC ADC Controller

The RTC ADC controller is a simplistic and low power controller for the ADC units that take a single reading and generate an interrupt when they are done.

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_REG to 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_FORCE bit of SENS_SAR_MEAS1_MUX_REG is unset, the output of the ADC RTC Reader is used for the SAR ADC operation
    • The ADC performs a SAR operation
      • sets the value in the low word of SEND_SAR_MEAS1_CTRL2_REG
      • sets the SENS_MEAS1_DONE_SAR when the operation is complete and the data is ready
      • fires an interrupt if SENS_SAR_READERx_CTRL_REG enables 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_FORCE bit of SENS_SAR_MEAS1_CTRL2_REG to 1, which configures the ADC to allow software start requests via the SENS_MEAS1_START bit
    • HAL sets the SENS_MEAS1_START bit of SENS_SAR_MEAS1_CTRL2_REG to 1, which forces the RTC ADC to fire
      • When the SENS_SAR1_DIG_FORCE bit of SENS_SAR_MEAS1_MUX_REG is unset, the output of the ADC RTC Reader is used for the SAR ADC operation
    • The ADC performs a SAR operation
      • sets the value in the low word of SEND_SAR_MEAS1_CTRL2_REG
      • sets the SENS_MEAS1_DONE_SAR when the operation is complete and the data is ready
      • fires an interrupt if SENS_SAR_READERx_CTRL_REG enables it

When an RTC operation is pre-empted by the arbiter on ADC2, the top two bits of SENS_MEAS2_DATA_SAR indicate this. 00 is a complete operation, 01 is a sample that did not start, 10 is a sample that did not complete.

Digital ADC Controller

The digital ADC controller is a higher power controller for the ADC that can do quite a bit more stuff. Specifically, it can do things like firing on a timer (separate from the system clock), continuously polling the ADC and updating with new values, and setting a threshold for the ADC values and firing an interrupt when the value is outside that threshold.

Hardware Registers

The ADC RTC hardware registers are mapped into memory between 0x6004_0000 and 0x6004_0FFF.

ADC Register Address Purpose
ADC1 APB_SARADC_CTRL_REG 0x6004_0000 Configuration register for DIG ADC controller
ADC2 APB_SARADC_CTRL2_REG 0x6004_0004 Configuration register for DIG ADC controller
ADC2* APB_SARADC_FILTER_CTRL1_REG 0x6004_0008 Configuration register 1 for SAR ADC filter
ADC1 APB_SARADC_APB_ADC_ARB_CTRL_REG 0x6004_0038 Configuration register for SAR ADC2 arbiter
ADC1 APB_SARADC_FILTER_CTRL0_REG 0x6004_003C Configuration register 0 for SAR ADC filter
ADC1 APB_SARADC_THRES0_CTRL_REG 0x6004_0044 Sampling threshold control register 0
ADC2* APB_SARADC_THRES1_CTRL_REG 0x6004_0048 Sampling threshold control register 1
ADC1 APB_SARADC_THRES_CTRL_REG 0x6004_0058 Threshold monitor enable register
ADC1 APB_SARADC_DMA_CONF_REG 0x6004_006C DMA configuration register for SAR ADC
ADC1 APB_SARADC_APB_ADC_CLKM_CONF_REG 0x6004_0070 Configure SAR ADC clock

* : I'm guessing somewhat here. The TRM didn't make it clear if these registers were for ADC1 or ADC2, they just numbered them 0 or 1.

Digital ADC Operation

The operation of the digital ADC is, on the surface, very similar to the RTC mode.

  • User calls analogContinuous or analogContinuousRead
    • HAL sets SENS_SAR1_DIG_FORCE bit of SENS_SAR_MEAS1_MUX_REG to 1, which tells the ADC1 mux to use the input from the digital reader
    • HAL configures the Digital ADC registers
      • HAL sets the configuration for the continuous digital operation in APB_SARADC_CTRLx_REG
      • HAL sets DMA configuration in APB_SARADC_DMA_CONF_REG
    • HAL sets the SENS_MEAS1_START_FORCE bit of SENS_SAR_MEAS1_CTRL2_REG to 1, which configures the ADC to allow software start requests via the SENS_MEAS1_START bit
    • HAL sets the SENS_MEAS1_START bit of SENS_SAR_MEAS1_CTRL2_REG to 1, which forces the RTC ADC to fire
    • The ADC performs a SAR operation
      • sets the value in the DMA data
      • sets the SENS_MEAS1_DONE_SAR when the operation is complete and the data is ready
      • fires an interrupt if SENS_SAR_READERx_CTRL_REG enables it

That's for the most simple case of the digital ADC controller operating on ADC1.

There are several features that are supported by the digital controller, and they don't appear to map into the analogContinuous* functions:

  • Scanning multiple channels. The ESP32-S3 digital ADC can scan up to 10 channels at a time, with different attenuation levels on each channel.
  • Thresholding. The ESP32-S3 digital ADC can generate an interrupt when the analog value is outside of a given threshold by setting threshold parameters in APB_SARADC_THRESx_CTRL_REG and APB_SARADC_THRES.
  • Filtering. The digital ADC automatically filters, but there are some extra filtering parameters you can set in APB_SARADC_FILTER_CTRLx_REG.
  • Mode control. From the architecture diagram above we can see that the digital ADC has two finite state machines connected to two digital readers. The digital ADC controller can sample a single reader, both readers can be sampled simultaneously, or it can flip-flop between FSMs/readers on even/odd requests.
  • Timers. You can set a timer and, after that given amount of time has elapsed, a sampling operation is performed and the output is stored via DMA somewhere.

When a digital operation is pre-empted by the arbiter on ADC2, the {sar_sel, ch_sel} bits in the DMA data indicate this. 1110 is a sample that did not start, 1111 is a sample that did not complete, and the data is valid when those two bits equal the channel number.

How SAR (Successive Approximation Register) Works

Now that we understand the overall architecture for the ESP32's ADC itself, and we know that it uses SAR, we can look into how its actual analog to digital conversion algorithm works. Don't worry, this isn't nearly as thick as everything else.

We know that ADC is, coarsely, a binary search looking for the correct digital representation of the analog voltage present on an input. We also know, from the Flash ADC breadboard we put together earlier in the project, that we can perform a comparison between two voltages with a comparator circuit; and that by successively dividing the voltage down, we can perform a comparison against a number with a given precision. (We built a 3 bit Flash ADC in our example for values from 0 to 7.) So by implementing a series of bitwise operations and saying "bigger than X?", we can eventually arrive at something that is close enough given our stated precision target. But with Flash ADC, in order to support the same precision we're getting from the ESP32-S3 ADC, we would need (I think) 4096 individual comparators. Plus we would need a precision resistor ladder, encoder logic, etc. It would be an enormous circuit. So Flash ADC is out of the question here.

SAR ADC solves these problems by using a remarkably small number of components:

  • One comparator with one input and one output
  • One DAC (Digital to Analog Converter)
  • A register big enough to hold a value of the precision being calculated
  • A logic circuit to run a for loop

The logic circuit, the register, and the DAC are used to drastically shrink the number of comparators that are needed, and to completely eliminate the need for the resistor ladder from the Flash DAC. The way it works is like this (assuming our 12 bit precision):

  • The register gets initialized with zero
  • For every bit in the register, beginning with the most significant bit and stepping down
    • Set the current bit to 1
    • The register is fed into the DAC
    • The DAC converts the input value to an output voltage
    • The comparator receives the DAC's output value, and compares it to the input analog voltage
    • Is the analog input voltage higher than the DAC voltage?
      • If yes, then we keep that bit turned on
      • If no, then we turn that bit off
    • Proceed to the next bit in the register

By doing this, we eventually wind up with a series of "less than greater than" comparisons that have toggled a bitfield in a 12 bit value to give us the nearest possible approximation to the input voltage that our register precision will support. It's quite elegantly simple, but not something you can build on a breadboard.

Final notes on the ESP32-S3 ADC

Aaaaaand I'm spent.

was it good for you too?

The TRM has a ton of information on these peripherals, but I'll be honest, if you're not used to reading TRMs, this can get thick quick. I needed a couple days of digging through the manuals to make sense of it all. But I'm glad I did.

Understanding capacitance and touch sensors

The last thing I wanted to understand here was ... How does the ESP32-S3 know when I'm touching that wire attached to the touch sensor?

The average touch sensor consists of a conductive surface connected to a GPIO dedicated to reading touch. But "touch" is not really measurable in an electronic sense. Touch sensors are really just measuring the capacitance of a given pathway.

Capacitance is a property of any two conductors separated by an insulator. We're probably used to thinking of capacitors as electronic components that have an anode and a cathode (+ and -) that cross a pair of plates with some kind of material between them (usually foil or an electrolyte). These devices work by storing charge as an electric field between the two plates, and they have the ability to discharge that current with great rapidity. Capacitance is a measure of how much charge can be stored in a particular pair of objects - whether that is a pair of plates, or your finger and a bare wire.

Consider the old trick of rubbing a balloon against your head, and then pulling it away. Your hair is attracted to the balloon because of static electricity. What you're actually seeing is that the balloon is charged one way, and your hair is charged another way. Specifically there is a difference in charge between your hair and the balloon. The hair loses electrons, while the balloon gains electrons. Because of this difference, charge exists; measurable voltage exists; and electric fields exist. But there is no significant current flowing (there may be electrons jumping here and there but it's not significant). When you move the balloon away and the hair rises to meet it, that isn't because of current, it's because the electric field generated exerts a force on the charges in the hair.

The balloon is essentially the same as how a capacitor works; there is a difference in the electrical charge between the two plates, and that creates an electrical field between them. Eventually the charge difference becomes great enough, and the electrical field strong enough, that lots of electrons jump all at once. It's a lot like how lightning works - the charge builds up so much that eventually the air breaks down, and ZAP, lightning.

But a capacitive touch sensor isn't exactly the same as a capacitor. All it is doing is acting as a sensor for electrical fields. The touch sensor itself is just some kind of conductive object that's connected to a circuit with some capacitors that have a known amount of capacitance. To read the sensor, the ESP32-S3 just discharges the capacitor and then detects how long before the capacitor is fully charged again. By doing this over and over, it can detect changes in the electrical field on the touch sensor, because the test capacitor will charge more or less slowly.

Why don't we need some special kind of touch sensor for this to work? I've spent a lot of time using "capacitive touch screens" and was given the impression this technology was something special. Just grabbing onto a bare wire seems ... cheap.

we have the technology

Since the touch sensor is just detecting changes in the rate of charge of a capacitor, all we need is something that will allow us to transmit electrons from one store (our bodies) to the other (the testing capacitor). This can be done by just getting the store (our fingertip) close enough to the wire to form the electrical field that's due to the charge imbalance; this will create a detectable change in the rate of charge of the capacitor. It doesn't even matter if we're grounded or not, because it isn't about completing a circuit with the sensor, it's about getting two charged objects near each other to induce that electrical field that induces detectable change in the capacitor.

Now obviously it doesn't ONLY work through the interaction of charged objects through the airgap via an electric field. It still works once we make real physical contact. When I look at the demonstration code running, I saw values around 52k from the touch sensor at idle with no contact. Lightly brushing the wire went up to around 63k. Firmly squeezing the wire brought it up to about 220k. Obviously physically attaching to the circuit will allow the charge variance to create actual current, which will much more rapidly change the rate at which the capacitor recharges after discharging, which will give us a stronger "touch" signal.