Long time, no see. I’ve been quite busy, and I had little time to write here. And I’ve spent the last days fighting to get one Voltmeter click to work with an Arduino Uno, as a prerequisite to the next project that will be featured on the blog.
The Voltmeter click board I’m writing about is a mikroBUS™ add-on board for measuring voltage in an external electric circuit. The board can measure DC only, and the advertised measurement range is -24 to 24V.
A very nice piece of hardware, but I have found that the accompanying code example is utterly wrong. If one looks at the code example one sees the following two lines of code:
measurement = getADC() / 2; // Get ADC result voltage = (measurement - calibration) * 33.3405;
Well, that 33.3405 value is wrong, it should be 16.67. Also, the measurement range is wrong.
To understand why we have to take an in-depth look at the schematic:

The input voltage goes through a voltage divider made with R1, R14, R15, and R2. The voltage drop on R2 is applied to the (non-inverting) input of OP1C, while the voltage drop on R15 goes to the input of OP1B.
The voltage drop on R14 is:
Same goes for R15:
When Vin is positive we will find the following situation:

With respect to GND, the voltage applied on pin 10 of OP1C is negative, and the output of OP1C is 0V. The voltage applied to pin 5 of OP1B is positive, and the output of OP1B is Vin * 0.03. The output of OP1B and VGND are summed by OP1D, and the output voltage going to the ADC will be:
A quick side note: VGND should have been named VBIAS, as not to be confused with GND. However, in this blog post I will keep the naming convention used in the original schematic.
When Vin is negative we will find the following situation:

With respect to GND, the voltage applied on pin 10 of OP1C is positive wrt GND, and the output of OP1C is -Vin * 0.03. OP1C acts basically as an inverter in this case, as its output is positive if the input voltage applied to the voltmeter click is negative. The voltage applied to pin 5 of OP1B is negative wrt GND, and the output of OP1B is 0. OP1D subtracts the output of OP1C from VGND, and the output voltage going to the ADC will be:
]
We see that, regardless of the polarity of the input signal, we will have the same relation between the input voltage and the voltage that goes to the ADC:
In theory, VGND is 1.024 V as this bias voltage is obtained by dividing the reference voltage Vref = 2.048V via R9 and R10. In practice, we can find some variations due to tolerance of R9 and R10. However, that difference is a unique characteristic of each voltmeter click board and can be measured only once.
One more thing: on a quick look at the MCP609 datasheet we find an absolute rating of Analog Inputs (VIN+, VIN–) as being VSS – 1.0V to VDD + 1.0V. In our case VSS is GND, and VSS can be 3.3V or 5V, depending on the position of the power select jumper on the voltage click.
So, one should not apply a negative voltage of more than 1V on the inputs of OP1C and OP1B. As such, we can determine the maximum voltage that can be applied to the inputs of the voltmeter click without causing permanent damage to the op-amps:
So, the actual measurement range of the voltmeter click is much higher than the advertised value. Nice.
The 33.3405 scaling factor is wrong!
Again, some calculations. We start by taking a look at the ADC converter. This click board uses an MCP3201 12-bit ADC, which reports a ratiometric value. This means that the ADC assumes Vref is 4095 and anything less than Vref will be a ratio between Vref and 4095.
As such, we can express the input voltage (in mV) as:
Or we can simply approximate:
But at the same time we have:
In the particular case of Vin = 0 we will find:
Then we will have:
However, we already know the offset corresponding to VGND, and we can replace it in the above formula:
And we can simply determine the input voltage:
Voltmeter click Arduino code
With all the above computation, things are deceptively simple. The most complicated part is to write the code for reading the ADC values. Luckily, I have already done that in my blog post regarding the 4-20mA receiver click board.
So, the first piece of code I wrote is used to determine the ADC offset. One has to put the input in shortcircuit, so only the bias voltage goes to the ADC. Then run the following code:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/******************************************************************* | |
____ __ ____ ___ ____ ____ __ __ _ ____ __ | |
( __)( ) ( __)/ __)(_ _)( _ \ / \ ( ( \(__ ) / _\ | |
) _) / (_/\ ) _)( (__ )( ) /( O )/ / / _/ / \ | |
(____)\____/(____)\___) (__) (__\_) \__/ \_)__)(____)\_/\_/ | |
Project name: Voltmeter click: tutorial & Arduino code | |
Project page: https://electronza.com/voltmeter-click-tutorial-arduino-code/ | |
Description : Voltmeter click calibration code | |
********************************************************************/ | |
#include <SPI.h> | |
// Arduino UNO with Mikroe Arduino Uno Click shield | |
// 4-20mA R click is placed in socket #2 | |
// CS is pin 9 | |
// SCK is pin 13 | |
// MISO is pin 12 | |
// MOSI is pin 11 | |
// Voltage click is placed in socket #1 | |
#define ADC_CS 10 | |
int ADC_result; | |
void setup() { | |
/* Resetting MCP3201 | |
* From MCP3201 datasheet: If the device was powered up | |
* with the CS pin low, it must be brought high and back low | |
* to initiate communication. | |
* The device will begin to sample the analog | |
* input on the first rising edge after CS goes low. */ | |
pinMode (ADC_CS, OUTPUT); | |
digitalWrite(ADC_CS, 0); | |
delay(100); | |
digitalWrite(ADC_CS, 1); | |
// initialize serial | |
Serial.begin(9600); | |
// initialize SPI | |
SPI.begin(); | |
} | |
void loop() { | |
ADC_result = read_raw(); | |
Serial.print("Conversion result: "); | |
Serial.println(ADC_result); | |
} | |
unsigned int read_raw(void){ | |
/* | |
DAC works on SPI | |
We receive 16 bits | |
Of which we extract only 12 bits | |
MCP3201 has a strange way of formatting data | |
with 5 bits in the first byte and | |
the rest of 7 bits in the second byte | |
*/ | |
unsigned int result; | |
unsigned int first_byte; | |
unsigned int second_byte; | |
unsigned int ADC_avrg = 0; | |
// Average of four readings | |
for (int i=0; i<4; i++){ | |
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE1)); | |
digitalWrite(ADC_CS, 0); | |
first_byte = SPI.transfer(0); | |
second_byte = SPI.transfer(0); | |
digitalWrite(ADC_CS, 1); | |
SPI.endTransaction(); | |
/* After the second eight clocks have been | |
sent to the device, the MCU receive register | |
will contain the lowest order seven bits and | |
the B1 bit repeated as the A/D Converter has begun | |
to shift out LSB first data with the extra clock. | |
Typical procedure would then call for the lower order | |
byte of data to be shifted right by one bit | |
to remove the extra B1 bit. | |
See MCP3201 datasheet, page 15 | |
*/ | |
result = ((first_byte & 0x1F) << 8) | second_byte; | |
result = result >> 1; | |
ADC_avrg = ADC_avrg + result; | |
// wait 10 ms between readings | |
delay(10); | |
} | |
ADC_avrg = ADC_avrg / 4; | |
Serial.print("Raw ADC value is: "); | |
Serial.println(ADC_avrg); | |
return ADC_avrg; | |
} |
For my particular voltmeter click board, I have found a value of ACDoffset = 2040 (vs. a theoretical 2048).
And now, the final code. I have decided to return the measured values in mV and to use int as data type. This will limit the measurement range to -32,768V to 32,767V, which is very close to the absolute ratings of the voltmeter click.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/******************************************************************* | |
____ __ ____ ___ ____ ____ __ __ _ ____ __ | |
( __)( ) ( __)/ __)(_ _)( _ \ / \ ( ( \(__ ) / _\ | |
) _) / (_/\ ) _)( (__ )( ) /( O )/ / / _/ / \ | |
(____)\____/(____)\___) (__) (__\_) \__/ \_)__)(____)\_/\_/ | |
Project name: Voltmeter click: tutorial & Arduino code | |
Project page: https://electronza.com/voltmeter-click-tutorial-arduino-code/ | |
Description : Voltmeter click demo code | |
********************************************************************/ | |
#include <SPI.h> | |
// Arduino UNO with Mikroe Arduino Uno Click shield | |
// 4-20mA R click is placed in socket #2 | |
// CS is pin 9 | |
// SCK is pin 13 | |
// MISO is pin 12 | |
// MOSI is pin 11 | |
// Voltage click is placed in socket #1 | |
#define ADC_CS 10 | |
int volts; | |
// this is determined emiprically, putting the input into short and reading the ADC readout | |
int ADC_offset = 2040; | |
void setup() { | |
/* Resetting MCP3201 | |
* From MCP3201 datasheet: If the device was powered up | |
* with the CS pin low, it must be brought high and back low | |
* to initiate communication. | |
* The device will begin to sample the analog | |
* input on the first rising edge after CS goes low. */ | |
pinMode (ADC_CS, OUTPUT); | |
digitalWrite(ADC_CS, 0); | |
delay(100); | |
digitalWrite(ADC_CS, 1); | |
// initialize serial | |
Serial.begin(9600); | |
// initialize SPI | |
SPI.begin(); | |
} | |
void loop() { | |
volts = read_volts(ADC_offset); | |
Serial.print("Measured voltage: "); | |
Serial.println(volts); | |
} | |
unsigned int read_raw(void){ | |
/* | |
DAC works on SPI | |
We receive 16 bits | |
Of which we extract only 12 bits | |
MCP3201 has a strange way of formatting data | |
with 5 bits in the first byte and | |
the rest of 7 bits in the second byte | |
*/ | |
unsigned int result; | |
unsigned int first_byte; | |
unsigned int second_byte; | |
unsigned int ADC_avrg = 0; | |
// Average of four readings | |
for (int i=0; i<4; i++){ | |
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE1)); | |
digitalWrite(ADC_CS, 0); | |
first_byte = SPI.transfer(0); | |
second_byte = SPI.transfer(0); | |
digitalWrite(ADC_CS, 1); | |
SPI.endTransaction(); | |
/* After the second eight clocks have been | |
sent to the device, the MCU receive register | |
will contain the lowest order seven bits and | |
the B1 bit repeated as the A/D Converter has begun | |
to shift out LSB first data with the extra clock. | |
Typical procedure would then call for the lower order | |
byte of data to be shifted right by one bit | |
to remove the extra B1 bit. | |
See MCP3201 datasheet, page 15 | |
*/ | |
result = ((first_byte & 0x1F) << 8) | second_byte; | |
result = result >> 1; | |
ADC_avrg = ADC_avrg + result; | |
// wait 10 ms between readings | |
delay(10); | |
} | |
ADC_avrg = ADC_avrg / 4; | |
return ADC_avrg; | |
} | |
int read_volts (int zero_offset){ | |
int myvolts; | |
int ADC_readout = read_raw(); | |
myvolts = (int)(float(ADC_readout – zero_offset) * 16.6667); | |
return myvolts; | |
} |
That’s all folks!