How to Calibrate Your Board's ADC With Arduino Manager

by Fab64 in Circuits > Electronics

167 Views, 0 Favorites, 0 Comments

How to Calibrate Your Board's ADC With Arduino Manager

FEGX1YKMPX8VEWF.png

This tutorial explains how to use the new ADC calibration features available in Arduino Manager: Two-Point Calibration and Lookup Table Calibration.

Both techniques are designed to improve ADC accuracy and linearity, but their effectiveness depends on the characteristics of the ADC being used. In this tutorial, we'll examine how each method works, compare their results, and determine which approach is best suited for different types of ADC errors.

Before diving into the details, let’s start with a brief introduction to Arduino Manager.

Arduino Manager, is a powerful general purpose iOS iPadOS / watchOS / macOS App which allows to control any Arduino (or Arduino compatible) board by the means of about 40 different Widgets.

Some of the available Widgets are:

  1. Display Widget
  2. LED Widget
  3. Switch Widget
  4. Push Button Widget
  5. Knob Widget
  6. Slider Widget
  7. Alarm Widget
  8. Sound Alarm Widget
  9. Thermostat Widget
  10. Threshold Widget
  11. Text Widget
  12. and many more

Arduino Manager prioritizes your privacy, with no registration or cloud account required—only your Apple ID. However, you have the option to save your Widget Lists to iCloud, enabling seamless sharing across your devices.

For more information about Arduino Manager:

  1. Arduino Manager for iOS, Arduino Manager for macOS.
  2. Past Instructables:
  3. XXXXX
  4. How to Control Arduino R4 WiFi From Your IPhone / Apple Watch / IPad / Mac
  5. Cheap IOT Device in Minutes and Without Writing Any Code Using Raspberry Pico Pi W
  6. Controlling ESP32 Running Micropython With Arduino Manager
  7. Arduino Manager: Generate Raspberry Pi Pico C/C++ Code for an AC Power Monitor – Step-by-Step Example
  8. How to Measure a Remote Temperature Using Pico W and Arduino Manager

When the complexity of the Arduino code increases, such as when dealing with multiple analog and digital sensors to read, controlling numerous actuators, and implementing complex algorithms, the likelihood of making errors rises. Consequently, debugging the code becomes more challenging and the time and effort required to address these issues could significantly affect the progress of your project. For this reason Arduino Manager also provides (for an additional small fee):

  1. Code Generator: This add-on allows to generate the basic Arduino code for supporting the Widgets you need in your project.
  2. PIN Code Generator: Linking widgets to Arduino PINs, this add-on dramatically speeds up Arduino code development. In many cases you don't need to write a single line of code.

A nice additional feature is Voice Commands which allows to control most of the Arduino Manager Widgets by means of your voice (English and Italian only)!. See past Instructables form more.

Apple Watch is supported as well (with some limitations): you can control any Arduino board directly from your wrist, wherever you are in the world!

Arduino Manager for iOS/iPadOS is available here: App Store for $7.99 and includes the watchOS version. The macOS version is available here: App Store for $10.99 (please, note that not all the widgets are available on all the platforms).

More information, documentation, video tutorials, libraries and examples on Arduino Manager are available here:

Arduino Manager for iOS or here: Arduino Manager for macOS.


For this Instructable you need Arduino Manager version 24.4.0 or greater.

Supplies

  1. Raspberry Pi Pico W (Pico 2W will also work)
  2. Seeed Studio XIAO ESP32S3 (any other ESP32 board will also work)
  3. DAC: MCP4822 (any DAC will work, but you will need to adapt the provided code for your specific device)
  4. breadboard
  5. Jump Wires
  6. DMM (the more accurate the measurement, the better the calibration results will be)
  7. Arduino Manager
  8. Arduino Manger Code Generator
  9. Arduino Manger Pin Code Generator

What Is an ADC a What Are Its Source of Errors

An Analog-to-Digital Converter (ADC) is a device that converts a continuous analog voltage into a discrete digital value that can be processed by a microcontroller or computer. In simple terms, it translates real-world signals—such as voltage from a sensor—into numbers that digital systems can understand.

An ideal ADC would produce a perfectly linear relationship between the input voltage and the output digital code. In practice, however, real ADCs are affected by several types of errors that limit their accuracy and resolution.

Sources of ADC Errors


1. Offset Error

Offset error occurs when the ADC does not output zero for zero input. This introduces a constant shift across the entire transfer curve.

2. Gain Error

Gain error is a scaling error that affects the slope of the conversion curve. Even if the ADC is linear, the full-scale value may not match the expected value.

3. Non-Linearity (INL/DNL)

  1. Integral Non-Linearity (INL) describes how far the ADC transfer function deviates from a straight line.
  2. Differential Non-Linearity (DNL) describes irregular spacing between adjacent digital codes and can lead to missing codes in extreme cases.

4. Quantization Error

Since an ADC has finite resolution, each digital output represents a range of input voltages. This introduces an unavoidable rounding error of up to ±½ LSB.

5. Noise

Thermal noise, power supply noise, and digital switching noise inside the microcontroller can cause random fluctuations in the measured value.

6. Temperature Drift

ADC characteristics change with temperature, affecting both offset and gain over time.

Understanding these error sources is essential when working with real-world measurements, especially when high precision is required. The calibration techniques presented in this tutorial—Lookup Table and Two-Point Calibration—are designed to compensate for some of these imperfections and improve measurement accuracy.


This Instructable shows how Arduino Manager helps correct deterministic ADC errors—offset error, gain error, and non-linearity—using calibration techniques.

In contrast, noise is handled separately through sample averaging, which reduces its impact but does not eliminate it.

General Theory: Two-Points Calibration

Before diving into further details, a brief explanation of the techniques used.

Some mathematics is involved, but if you are not used to it, don’t worry—Arduino Manager handles most of the calculations for you automatically.


Two-point calibration is a simple method used to correct offset and gain errors in an ADC by measuring the system response at two known reference points.

The idea is straightforward: instead of assuming that the ADC transfer function is ideal, we measure how it actually behaves at two different known input values and use these measurements to reconstruct a corrected linear mapping.

Typically, one point is chosen near the low end of the measurement range (close to 0 V), and the second point is chosen near the high end (close to full scale). These two points define a straight line that approximates the real ADC behavior.


How it works


Let:

  1. V1​ and V2 be two known input voltages
  2. D1 and D2 be the corresponding ADC readings

We assume a linear relationship of the form:

Vcorrected = a⋅m +b [1]

Where:

  1. m is the gain correction factor (slope)
  2. b is the offset correction (intercept)

Using the two calibration points, we compute:

a =(V2−V1) / (D2−D1)

b = V1−a⋅D​1

Once m and b are known, every future ADC reading D can be converted into a corrected voltage using the same linear equation [1]


What it corrects (and what it does not)


Two-point calibration is very effective at correcting:

  1. Offset error
  2. Gain error

However, it cannot correct:

  1. Non-linearity (INL/DNL errors)
  2. Noise (which must be handled separately, e.g., by averaging)

General Theory: Lookup Table

Lookup Table (LUT) calibration is a method used to correct non-linearity, offset, and gain errors in an ADC by mapping measured digital values to known reference values across multiple points in the input range.

Unlike two-point calibration, which assumes the ADC behaves like a perfect straight line, lookup table calibration does not assume linearity. Instead, it builds a piecewise correction curve based on real measurements.


How it works


The idea is to divide the ADC input range into multiple known reference points and measure the corresponding ADC output values at each point.

For example, you apply a set of known voltages:

  1. V0,V1,V2,…,Vn

and record the corresponding ADC readings:

  1. D0,D1,D2,…,Dn

These pairs form a calibration table.


Using the lookup table


When a new ADC reading D is acquired, the corrected value is computed by:

  1. Finding the two calibration points (Di,Vi) and (Di+1,Vi+1) such that:
  2. Di≤D≤Di+1
  3. Applying linear interpolation between them:


Vcorrected = Vi + (D−Di) / (Di+1−Di) ⋅ (Vi+1−Vi) [2]


This allows the system to approximate a continuous calibration curve even though only discrete points are measured.

What it corrects (and what it does not)


Lookup table calibration is capable of correcting:

  1. Offset error
  2. Gain error
  3. Non-linearity (INL/DNL), within the resolution of the table

However, it does not directly remove:

  1. Random noise (which must still be reduced by averaging)
  2. Errors outside the calibrated range

General Workflow

The general workflow for understanding how Arduino Manager can help improve ADC accuracy is as follows:

  1. We will build a simple ADC test circuit controlled by Arduino Manager.
  2. The circuit will generate a voltage sequence from 0 to 3.3 V using a microcontroller.
  3. We will then create a Widget List, configure the Arduino IDE project or the C Pico project based on the native SDK, and generate the code to handle the widgets in the Widget List.
  4. At this point, we will be ready to perform the measurements needed for two-point calibration.
  5. Next, we move to the more repetitive part: collecting measurements for the lookup table.
  6. Once the data is collected and imported into Arduino Manager, the lookup table will be generated automatically.
  7. We can then compare the results and evaluate whether the extra effort is worth it.

To better understand both techniques, we will use two boards:

  1. Raspberry Pi Pico W/2W: ADC with good linearity but noticeable offset and gain errors
  2. ESP32: ADC known for significant non-linearity

The Measurement Circuit

ESP32S3_Schematic.png
Pico_Schematic.png

The first measurement circuit (Fig 1) is based on a Xiao ESP32S3 interfaced with an MCP4822 dual-channel 12-bit Digital-to-Analog Converter (DAC) via the SPI communication bus. The purpose of the circuit is to generate a programmable analog voltage that can be used as a reference, excitation signal, or control input within the measurement system.

Only Channel A of the MCP4822 is utilized in the current design. The analog output VA (Pin 8) is routed to:

  1. GPIO9 of the Raspberry ESP32S3
  2. Test point TP1 for measuring the voltage using the external DMM

This arrangement allows the ESP32S3 to generate a known analog voltage through the DAC and subsequently measure it using its internal Analog-to-Digital Converter (ADC).

The measurement circuit for Raspberry Pico W/2W (Fig 2) is identical except for the microcontroller and pin assignments.

The Arduino Manager Widget List

Screenshot 2026-06-15 at 9.32.23 AM.png

To save time, we will use the attached Widget List. If you want to learn how to create it from scratch, you can read the other Instructables about Arduino Manager or consult its documentation.

To load the file in Arduino Manager procede with the following steps.


macOS


  1. Download the ADC Calibration.wl (I used an external shared file because Instructables does not allow files with unknown extensions to be uploaded)
  2. Open Arduino Manager
  3. From the side menu, select Widget Lists
  4. From the toolbar click Import and select the downloaded file


iPhone / iPad


  1. Download the ADC Calibration.wl (I used an external shared file because Instructables does not allow files with unknown extensions to be uploaded)
  2. Open Arduino Manager
  3. From the side menu, select Widget Lists
  4. From the toolbar click Import and select the downloaded file


Create the Connection to the board


Although no firmware is running on the board yet, we can create the two connections that will be used later.

  1. From side menu select Connections.
  2. New Connection (+)
  3. Enter the following values:
  4. Storage: Cloud
  5. Name: ADC - ESP 32
  6. IP: 192.168.1.150 (adapt to your network characteristics)
  7. Port: 8080
  8. Tap Save.
  9. New Connection (+) again.
  10. Enter the following values:
  11. Storage: Cloud
  12. Name: ADC - Pico
  13. IP: 192.168.1.151 (adapt to your network characteristics)
  14. Port: 8080
  15. Tap Save.


Select the Connection


Now we can select the connection in the Widget List. Since, we will start with the ESP32 board, we will select the connection to that board first.

  1. From the side menu select Widgets.
  2. Switch to Edit Mode.
  3. Click on the gear icon on the top right side of the screen.
  4. Set Connection to ADC - ESP 32.
  5. Switch back to Normal Mode.


The Widget List description


The widget list is composed of 5 Display Widgets that show the measured values:

  1. DMM: value measured by the DMM.
  2. ADC: raw value provided by the ADC.
  3. Voltage: voltage value calculated from the ADC reading.
  4. Voltage using LUT: corrected voltage value calculated using a Lookup Table (LUT).
  5. Voltage using TPC: corrected voltage value calculated using Two-Point Calibration (TPC).

Three additional Display Widgets show the error relative to the DMM measurement:

  1. Error %
  2. Error LUT %
  3. Error TPC %

Finally, there are three Push Button Widgets:

  1. Up: to increase the DAC output voltage.
  2. Down: to decrease the DAC output voltage.
  3. Reset: to reset the DAC output voltage to its initial value.

The DAC Input Field Widget can be used to set the DAC to a specific voltage directly.

Correct ESP32 ADC Readings

We will start with an ESP32 board and, with the help of Arduino Manager, analyze its ADC behavior without any correction, then compare it with readings corrected using Two-Point Calibration and a Lookup Table.

Arduino Manager provides both the Code Generator and the Pin Code Generator, which work together to generate code for the target board from the widgets included in the widget list.

This approach saves a considerable amount of programming time while minimizing the risk of errors.

Furthermore, when an ADC is involved, the generators significantly reduce the effort required to create a Lookup Table or calculate the parameters needed for Two-Point Calibration.

Let's get started.

  1. From the side menu select Code Generator Projects.
  2. Create a new project (+).
  3. Enter the following information:
  4. Name: ADC Calibration - ESP32
  5. Board: ESP32-WiFi
  6. Communication Device: Built in WiFi
  7. Language: C
  8. Connection: ADC - ESP32. Some fields are automatically filled.
  9. Netmask: 255.255.255.0 (adapt to your network characteristics)
  10. Gateway: 192.168.1.1 (adapt to your network characteristics)
  11. SSID: name of your network
  12. Password: password to your network
  13. Connection Callback: enabled
  14. Disconnection Callback: enabled
  15. Select Widget Pins tab.
  16. Select the Display Widget named ADC and enter the following information:
  17. Pin: 9 (according to the schematic)
  18. Ready Every: 700
  19. Averaging: enabled
  20. Number of Samples: 64
  21. Select the Display Widget named Voltage and enter the following information:
  22. Pin: 9 (according to the schematic)
  23. Ready Every: 700
  24. Averaging: enabled
  25. Number of Samples: 64
  26. Convert to voltage: enabled
  27. Vref: 3.3
  28. Click Generate.
  29. Click Save and Close.

Now the project is generated and ready to be uploaded to the board.

For now we have configured the Display that shows the raw ADC reading and the voltage reading converted using the basic formula well know formula:


V = ADCreading * Vref / (2^n - 1)


In the next steps we collect data for the Two-Points calibration and for the Lookup table.

Correct ESP32 ADC Readings - Reading Values From DMM

If your DMM supports SCPI protocol protocol over Ethernet, you can have its readings directly available in Arduino Manager. Otherwise, you will need to read them from the DMM display.

To read the DMM directly from the ESP32 board follow this steps:


  1. Locate the section
/* Start your code (defines) */
/* End your code */

and add this code:

/* Start your code (defines) */

WiFiClient dmmClient;
const char *dmm_ip = "192.168.1.220"; // your DMM IP address
const int dmm_port = 5025; // your DMM IP Port

boolean dmmConnected = false;
String disconnectionCommand;
String measurementCommand;

/* End your code */


Adapt the ip address to the one used by your device.

  1. Locate the section
/* Start your code (User Functions) */
/* End your code */

and add this code:

/* Start your code (User Functions) */

void dmmConnect() {
if (!dmmClient.connect(dmm_ip, dmm_port)) {
Serial.println("Connection to DMM failed");
return;
}

dmmSendSCPICommand("*IDN?");
delay(200);

String resp = dmmReadResponse();
printf("Response: %s\n", resp.c_str());
dmmConnected = true;

if(resp.indexOf("SDM3055") > 0) {
disconnectionCommand = "*RST";
measurementCommand = "MEAS:VOLT:DC?";
}

/*
if(resp.indexOf("XXXXX") > 0) {
disconnectionCommand = "XXXX";
measurementCommand = "XXXX";
}
*/
}

void dmmDisconnect() {
dmmConnected = false;

printf("Disconnection Command %s\n",disconnectionCommand);
dmmSendSCPICommand(disconnectionCommand);

delay(400);
dmmConnected = false;

dmmClient.stop();
}

float dmmRead() {
dmmSendSCPICommand(measurementCommand);
delay(200);
String reading = dmmReadResponse();
float voltage = atof(reading.c_str());
return voltage;
}

void dmmSendSCPICommand(const String &cmd) {
dmmClient.print(cmd);
dmmClient.print("\n");
}

String dmmReadResponse() {
String r = "";
unsigned long t0 = millis();
while (millis() - t0 < 1000) {
while (dmmClient.available()) {
char c = dmmClient.read();
r += c;
}
}
return r;
}

/* End your code */


Without getting into details, DMM is accessed via TCP/IP and the reading is started issuing a specific command.

We are assuming that DMM used for the measurements is the Siglent SDM3055, you may need to adapt the code for different DMMs here:

if(resp.indexOf("XXXXX") > 0) {
disconnectionCommand = "XXXX";
measurementCommand = "XXXX";
}


  1. Locate the doWork section and add this code to periodically the voltage using the DMM
void doWork() {

....

/* Start your code (doWork) */

if (dmmConnected) {
if ((millis() - dmmLastMeasurementTime) > 1000) {
dmmLastMeasurementTime = millis();
dMM = dmmRead();
}
}

/* End your code */
}

Correct ESP32 ADC Readings - Completing the Firmware

We need to add some additional code to control the ADC using the Push Button Widget

At the top of the code add the following lines:


/* Start your code (defines) */

#include "MCP_DAC.h"

MCP4822 MCP;

#define PIN_MOSI 4
#define PIN_SCK 6
#define PIN_CS 3

uint16_t initialDac = 28;
uint8_t dacStep = 100;

WiFiClient dmmClient;
....

/* End your code */


and in the setup function add:


/* Start your code (setup) */

SPI.begin(PIN_SCK, -1, PIN_MOSI, PIN_CS);

pinMode(PIN_CS, OUTPUT);
digitalWrite(PIN_CS, HIGH);

MCP.begin(PIN_CS);
MCP.setGain(2);

dac = initialDac;

MCP.write(dac, 0);
MCP.write(dac, 1);

/* End your code */


pushButtonUpCallback

/* Start your code (pushButtonUpCallback) */

if (pushed) {
dac += dacStep;
MCP.write(dac, 0);
printf("DAC %d\n", dac);
}

/* End your code */


pushButtonDownCallback

/* Start your code (pushButtonDownCallback) */

if (pushed) {
dac -= dacStep;
MCP.write(dac, 0);
printf("DAC %d\n", dac);
}

/* End your code */


pushButtonResetCallback

/* Start your code (pushButtonResetCallback) */

if (pushed) {
dac = initialDac;
MCP.write(dac, 0);
}

/* End your code */


Eventually, we have to handle the Input Field Widget in order to set the DAC at any desired value:


textFieldDACCallback

/* Start your code (textFieldDACCallback) */

MCP.write(dac, 0);

/* End your code */


Open the code using the Arduino IDE, then upload it to your board.

Collecting Data for Two-Points Calibration

FGNDFENMPX8VPGK.png
FNNAK5YMPX8VPO0.png
Figure__3.png
Figure_4.png

Eventually, we are ready to collect some useful data. For these purpose a template in Excel is available for download:

Calculation Template

Using the Push Button Widgets and the Input Field Widget, we can set ADC values across the entire 0–3.3 V range according to the values listed in Table 1. For each value, enter both the DMM Measured Voltage and the ADC Measured Voltage as displayed in Arduino Manager.

We observe that the relative error is high across the entire measurement range, making the readings almost unusable (Figure #1 and Figure #2).

Please note: The values shown in this table and the following tables in this Instructable are specific to my setup, including the microcontroller, circuit, DAC, USB supply voltage, DMM, and other measurement conditions. Your readings will likely differ, but the overall behavior should be similar.

To correct the readings, we apply a two-point calibration using the first reading in the table, which is close to 0 V, and the last reading, which is close to the maximum input voltage accepted by the ADC.

  1. Select Code Generator Projects using the side menu.
  2. Select the project: ADC Calibration - ESP32 and edit it.
  3. Select the Widget Pins tab.
  4. Select the widget named: Voltage Using TPC and enter the following information:
  5. Pin: 9 (according to the schematic)
  6. Ready Every: 700
  7. Averaging: enabled
  8. Number of Samples: 64
  9. Convert to voltage: enabled
  10. Vref: empty
  11. Use Two-Points Calibration: enabled
  12. Min Measured: 9
  13. Min Expected: 0.029
  14. Max Measured: 4093
  15. Max Expected: 3.143
  16. Click Generate and then Save & Close.
  17. Upload the code to the board

After filling in the Two-Point Calibration Corrected ADC Measured Voltage column in Table 1, we can see that the error has changed shape but has not been reduced. On the contrary, in some cases, it is even worse (Figure #3 and Figure #4).

As mentioned earlier, two-point calibration provides better performance when the ADC is linear but exhibits offset and gain errors.

Collecting Data for the Lookup Table

FX8PVDJMPX8VSOP.png
F3AHI2KMPX8VSP0.png

Unfortunately, this section is quite boring because a lot of value has to be collected. It worths the effort.

First we have to collect much more measurements points to have a reasonable precision. Using the buttons we collect the value to fill the Table 2 of the template.

Now we need all that values in a csv file for computing the Lookup table

  1. Copy the columns: ADC, DMM Measured Voltage in a new Excel sheet.
  2. File -> Save As ...
  3. Select the project folder, file format CSV UTF-8 and a file name (e.g. readings.csv)

Note: After saving, Excel switches to the CSV file. Close the CSV file and reopen the original Excel workbook to continue.

We can not get back to Arduino Manager:

  1. Select Code Generator Projects using the side menu.
  2. Select the project: ADC Calibration - ESP32 and edit it.
  3. Select the Widget Pins tab.
  4. Select the widget named: Voltage Using LUT and enter the following information:
  5. Pin: 9 (according to the schematic)
  6. Ready Every: 700
  7. Averaging: enabled
  8. Number of Samples: 64
  9. Convert to voltage: enabled
  10. Vref: empty
  11. Use Lookup Table: enabled
  12. Select and choose the previously saved CSV file
  13. Lookup Table Steps: 1 (the final lookup will not be compressed)
  14. Use Two-Points Calibration: disabled
  15. Click Generate and then Save & Close.
  16. Upload the code to the board

We can now fill the column LUT corrected ADC Measured voltage of Table 1 and we can see that the relative error is drastically reduced (Figure #5 and Figure #6).

Conclusion for ESP32

Due to the nonlinearity of the ESP32 DAC, using a lookup table is the most effective way to correct the ADC readings for this device.

Once the calibration data has been collected, the Arduino Manager Pin Code Generator automatically generates all the required code for you.

Correct Raspberry Pico ADC Readings - Preparing the Widget List

We now move on to the Raspberry Pi Pico ADC. We essentially need to repeat the work we did for the ESP32. To save time, we can copy the existing project and modify it.

  1. From the side menu open Code Generator Projects.
  2. Select ADC Calibration - ESP32.
  3. Duplicate it.
  4. Select ADC Calibration - ESP32 Copy and open it.
  5. Make the following changes:
  6. Name: ADC Calibration - Pico
  7. Board: Raspberry Pico 2 (or 2 W according to your availability)
  8. Language: C++ (native SDK)
  9. Connection: ADC-Pico (Ip and port are adapted according on the connection definition):
  10. Password: password to your network
  11. Connection Callback: enabled
  12. Disconnection Callback: enabled
  13. Select the Widget Pins tab
  14. Select the Widget named ADC and set the Pin to 27
  15. Select the Widget named Voltage and set the Pin to 27
  16. Select the Widget named Voltage using TPC and set the Pin to - (no data is available for the correction yet)
  17. Select the Widget named Voltage using Lookup Table and set the Pin to - (no data is available for the correction yet)
  18. Click Generate and then Save & Close.
  19. Open Visual Studio Code and import the project. If you are not familiar with setting up an Arduino Manager–generated Pico project in VS Code, refer to the Arduino Manager documentation under Raspberry Pi Pico W / 2 W Wi-Fi – C/C++ SDK.
  20. Don't forget to switch the connect to ADC - ESP Pico that we have already created.

Correct Raspberry Pico ADC Readings - Reading Values From DMM

To read the DMM directly from the Pico W/2W board follow this steps:

  1. Download the following files and copy them in your project folder. They provide some basic functions to connect and transfer data via tcp/ip
  2. tcp_client.h
  3. tcp_client.c
  4. Locate the section
/* Start your code (defines) */
/* End your code */

and add this code:

#include "hardware/spi.h"
#include "MCP48xx.h"

extern "C"
{
#include "tcpip_client.h"
}

// SPI port configuration for MCP4822

#define SPI_PORT spi1
#define PIN_MISO 12 // MCP4822 not used
#define PIN_CS 9 // MCP4822 pin 2
#define PIN_SCK 10 // MCP4822 pin 3
#define PIN_MOSI 11 // MCP4822 pin 4

MCP4822 dac_device(PIN_CS);

uint16_t initialDac = 28;
uint8_t dacStep = 100;

#define DMM_IP "192.168.1.220" // your DMM IP address
#define DMM_PORT 5025 // your DMM IP Port

static bool connectDMM = false;
static TCP_CLIENT_T *state;
static bool dmmConnected = false;
static char disconnectionCommand[32];
static char measurementCommand[32];
static unsigned long dmm_last_measurement_time;

void dmmConnect();
void dmmDisconnect();
float dmmRead();

/* End your code */


  1. Locate the section
/* Start your code (setup) */
/* End your code */

and add this code:

/* Start your code (setup) */

// SPI initialisation. This example will use SPI at 1MHz.
spi_init(SPI_PORT, 20 * 1000);
gpio_set_function(PIN_MISO, GPIO_FUNC_SPI);
gpio_set_function(PIN_CS, GPIO_FUNC_SIO);
gpio_set_function(PIN_SCK, GPIO_FUNC_SPI);
gpio_set_function(PIN_MOSI, GPIO_FUNC_SPI);
gpio_set_dir(PIN_CS, GPIO_OUT);
gpio_put(PIN_CS, 1);

dac_device.init(SPI_PORT);
dac_device.turnOnChannelA();
dac_device.turnOnChannelB();
dac_device.setGainA(MCP4822::High);
dac_device.setGainB(MCP4822::High);
dac_device.setVoltageB(0);
dac_device.updateDAC();

dac = initialDac;
dac_device.setVoltageA(dac);
dac_device.updateDAC();

/* End your code */


  1. Locate the section
/* Start your code (User Functions) */
/* End your code */

and add this code:

/* Start your code (User Functions) */

void dmmConnect()
{
state = tcp_client_init(DMM_IP, DMM_PORT);

if (!state)
{
return;
}

if (!tcp_client_open(state))
{
tcp_result(state, -1);
return;
}

while (!state->connected)
{
sleep_ms(10);
}

char response[128];
err_t err = tcp_client_send_and_wait(state, (uint8_t *)"*IDN?\n", 6, response, sizeof(response), 5000);
if (err != ERR_OK)
{
printf("Error %d\n", err);
return;
}

if (strstr(response, "SDM3055"))
{
strcpy(disconnectionCommand, "*RST\n");
strcpy(measurementCommand, "MEAS:VOLT:DC?\n");
}
/*
if (strstr(response, "XXXX"))
{
strcpy(disconnectionCommand, "*RST\n");
strcpy(measurementCommand, "MEAS:VOLT:DC?\n");
}
*/
dmmConnected = true;
}

void dmmDisconnect()
{
strncpy((char *)state->buffer, disconnectionCommand, BUF_SIZE - 1);
state->buffer[BUF_SIZE - 1] = '\0';
state->buffer_len = strlen((char *)state->buffer);
tcp_client_send(state);

sleep_ms(400);
dmmConnected = false;

tcp_client_close(state);
}

void dmmSendSCPICommand(const char *cmd, char *response, uint8_t response_size)
{
err_t err = tcp_client_send_and_wait(state, (uint8_t *)cmd, strlen(cmd), response, response_size, 5000);
if (err != ERR_OK)
{
printf("Error %d\n", err);
}
}

float dmmRead()
{
char response[128];
dmmSendSCPICommand(measurementCommand, response, 128);

float voltage = atof(response);
// printf("String reading %s - Float %.4f\n", reading.c_str(), voltage);
return voltage;
}

/* End your code */


  1. locate this section
/* Start your code (doWork) */
/* End your code */

and add this code;

if (dmmConnected)
{
if ((time_us_64() / 1000 - dmm_last_measurement_time) > 500)
{
dmm_last_measurement_time = time_us_64() / 1000;
adc_select_input(1);
dMM = dmmRead();
}
}

if (connectDMM)
{
connectDMM = false;
dmmConnect();
}


  1. locate
/* Start your code (deviceConnected) */
/* End your code */

and add this code:

/* Start your code (deviceConnected) */
printf("Device Connected\n");
connectDMM = true;
/* End your code */

Correct Raspberry Pico ADC Readings - Completing the Firmware

We need to add some additional code to control the ADC using the Push Button Widget


/* Start your code (deviceDisconnected) */
/* End your code */

and add this code:

/* Start your code (deviceDisconnected) */
printf("Device disconnected\n");
dmmDisconnect();

dac = initialDac;
dac_device.setVoltageA(dac);

/* End your code */


/* Start your code (pushButtonUpCallback) */
if (pushed)
{
dac += dacStep;
dac_device.setVoltageA(dac);
dac_device.updateDAC();

printf("DAC %d\n", dac);
}
/* End your code */


/* Start your code (pushButtonDownCallback) */
if (pushed)
{
dac -= dacStep;
dac_device.setVoltageA(dac);
dac_device.updateDAC();

printf("DAC %d\n", dac);
}
/* End your code */


/* Start your code (pushButtonResetCallback) */
if (pushed)
{
dac = initialDac;
dac_device.setVoltageA(dac);
dac_device.updateDAC();
printf("DAC %d\n", dac);
}
/* End your code */


/* Start your code (textFieldDACCallback) */
dac_device.setVoltageA(dac);
dac_device.updateDAC();
/* End your code */


Collecting Data for Two-Points Calibration

F4QU4WIMQIJAGV5.png
FL50W48MQIJAHI4.png
Screenshot 2026-07-07 at 8.51.53 AM.png
F57FU3WMQIJAHJ1.png

As we did for the ESP32 device, using the Push Button widgets and the Input Field widget, we can set a few discrete ADC values over the entire 0–3.3 V range, producing the values shown in Figure #7 and Figure #8.

We see that the precision of the Pico’s ADC is generally better than the ESP32’s ADC precision. Nevertheless, we can try to improve the results by starting with a Two-Points Calibration using the first reading in the table (which is close to 0 V) and the last one (which is close to the maximum voltage accepted by the ADC).

  1. Select Code Generator Projects using the side menu.
  2. Select the project: ADC Calibration - Pico and edit it.
  3. Select the Widget Pins tab.
  4. Select the widget named: Voltage Using TPC and enter the following information:
  5. Pin: 9 (according to the schematic)
  6. Ready Every: 700
  7. Averaging: enabled
  8. Number of Samples: 64
  9. Convert to voltage: enabled
  10. Vref: empty
  11. Use Two-Points Calibration: enabled
  12. Min Measured: 38
  13. Min Expected: 0.031
  14. Max Measured: 3837
  15. Max Expected: 3.092
  16. Click Generate and then Save & Close.
  17. Compile and run the code to the board

Filling the column Two-Points Calibration corrected ADC Measured Voltage of Table 1 we see that the error is reduced especially for low voltages taking advantage of the Pico's ADC linearity (Figure #9 and Figure #10)

Collecting Data for Lookup Table

Fig_11.png
Fig_12.png

Now we can try to get even better results using a LookUp Table.

First we have to collect much more measurements points to have a reasonable precision. Using the buttons we collect the value to fill the Table 2 of the template.

Now we need all that values in a csv file for computing the Lookup table

  1. Copy the columns: ADC, DMM Measured Voltage in a new Excel sheet.
  2. File -> Save As ...
  3. Select the project folder, file format CSV UTF-8 and a file name (e.g. readings.csv)

Note: After the save Excel switched to the CSV file. You need to close it and open the original Excel file again.

We can not get back to Arduino Manager:

  1. Select Code Generator Projects using the side menu.
  2. Select the project: ADC Calibration - Pico and edit it.
  3. Select the Widget Pins tab.
  4. Select the widget named: Voltage Using LUT and enter the following information:
  5. Pin: 27 (according to the schematic)
  6. Ready Every: 700
  7. Averaging: enabled
  8. Number of Samples: 64
  9. Convert to voltage: enabled
  10. Vref: empty
  11. Use Lookup Table: enabled
  12. Select and choose the previously saved CSV file
  13. Lookup Table Steps: 1 (the final lookup will not be compressed)
  14. Use Two-Points Calibration: disabled
  15. Click Generate and then Save & Close.
  16. Upload the code to the board

We can now populate the LUT Corrected ADC Measured Voltage column in Table 1. As shown, the relative error does not change significantly, but it is generally lower than that obtained with the Two-Point Calibration (Figure #11 and Figure #12).

Final Conclusions

Arduino Manager provides three main techniques to improve the accuracy of ADC readings:

  1. Averaging
  2. Two-point calibration
  3. Lookup table

These techniques, together with the code generators available in Arduino Manager, make it extremely quick and easy to improve ADC accuracy without writing code.

Two-point calibration requires very little effort, as it only involves measuring two points. However, it is effective only if the ADC has an approximately linear response, as is the case with the Raspberry Pi Pico's ADC. In contrast, a lookup table can significantly improve accuracy, especially when the ADC response is non-linear, as with the ESP32's ADC. The trade-off is that it requires considerably more effort because a much larger number of measurements must be taken.


Whichever correction technique you choose, Arduino Manager can handle the complexity for you:

  1. No correction: Arduino Manager uses the amController's to_voltage() function to automatically convert ADC integer readings into voltage.
  2. Two-point calibration: Arduino Manager uses the amController's linearization functions and automatically calculates the calibration coefficients to minimize offset and gain errors.
  3. Lookup table: Arduino Manager uses the DMM readings you provide to automatically generate a lookup table that compensates for the ADC's non-linearity, resulting in a more accurate voltage measurement.