PCF8591: A Four input ADC with additional DAC
output. How good is this 8 bit ADC, and should you use it? Find out the
true conversion speed here.
The PCF8591:
Is a four input, 8 bit ADC.
Has one DAC output.
Can be configured for 2 differential inputs.
Is slow! (compared to the Arduino Uno ADC).
This is an ADC with 4 analogue inputs which can be used as two
differential inputs. It uses the I2C communications interface and performs a
successive approximation conversion process - the same as the operation
of the ADC in the Arduino. It also has a DAC, allowing true analogue output.
PCF8591 YL-40 Breakout board module
The ADC may be a good choice
if you need a few more analogue inputs for your project, as long as you
don't require high resolution (8bit) - There are also other limitations (sample rate speed).
Note: This ADC (x4) has only 8 bit resolution but includes a DAC (x1)!
The basic update rate from a
single ADC is 11kHz - comparable to the Arduino ADC - but just a bit faster [No it is slower see Analysis and test results here].
One unusual capability of this device is the analogue output using the internal DAC (8bit resolution). However there are limitations to the DAC.
Warning: The DAC output will not go above 0.9xVcc with a 10k load.
You can add up to seven additional chips onto the same I2C bus
giving you a total of 32 analogue ADC inputs, and 8 DAC analogue outputs.
The PCF8591 also has a separate ground input. With this input you can keep analogue and digital
grounds
separate (and only connect them at the power supply ground). This stops
digital noise leaking into the analogue circuit so you get a better
result since dynamic noise is eliminated.
The pinout for the DIP version and SO16 (SMD) are identical:
[Source:
Datasheet]
PCF8591 Inputs
The four ADC channels can be configured as single ended or
differential inputs. For single ended input, these work the same as an
Arduino ADC input i.e. they measure the input signal compared to ground.
For a differential reading, the difference between two ADC inputs is
measured (using an internal op-amp to subtract one signal from the
other). This is something you can't do with an Arduino as all ADC inputs
are referenced to ground (there are no internal op-amps in Arduinos
except ATTiny85 which can make a differential ADC measurement).
There
are two reasons you may want to do this:
1. Measure the voltage across a component (when no end is connected to ground).
2. Reduce noise using differential inputs - the same noise voltage will be localised across a resistance and therefore eliminated.
3. Reduce noise by measuring away from ground bounce
(see option 01 in the diagram below) of using a virtual ground (see below).
Warning: The differential input signal range is halved. You can only have signals in the range -Vfs/2 ~ Vfs/2.
Ground Bounce
Ground bounce is caused by sinking and sourcing of currents in the
digital system. When a digital signal changes state current is drawn and diverted to the ground
plane to return to the negative power supply.
The
small resistance of the ground plane, between one pin and another,
causes a small voltage drop - but add multiple changes i.e. in
microcontroller and you can get a large ground bounce. Since a single
ended ADC measurement operates by comparing the input voltage to ground,
the ground bounce causes an error.
Using a separate ground
plane reduces digital ground bounce for the analogue circuitry (since the
two planes - digital ground and analogue ground - are only connected at
the power supply input). Therefore digital noise is not leaked into the
analogue ground plane.
Another method of avoiding noise is to create a virtual ground
using an opamp and reference signals to that op-amp output. The op-amp
can be used in this way using mode 01 (below); by feeding the op-amp
output into AIN3 below.
You can also mix and match single ended or differential input ability with the PCF8591. For example you can have
two single ended inputs (referenced to ground) and one differential
input. The table below shows the programmable options available.
[Source: Datasheet]
PCF8591 I2C Address's
The lower three bits of the address consist of the three digital
inputs A2, A1, A0 while the upper bits are fixed at 1001xxx. The the last
bit( LSB ' 'L) is ignored as it is the read write bit (R/Wn). Therefore
the addresses available are:
0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F.
There are eight individual addresses selected by setting the inputs A2..A0 high or low in hardware.
PCF8591 DAC Output
The PCF8591 has an internal DAC (Digital to Analogue Converter) which
creates a true analogue signal having 256 steps. It works by
switching in different internal resistances. So unlike the Arduino
analogue signal that uses a PWM signal (and requires a filter capacitor)
this is a true analogue output.
The DAC is required to perform analogue
to digital conversion, generating an analogue signal that is compared to
the input signal (See operation of successive approximation ADC here). In the PCF8591 this analogue signal can also be output directly to the
Aout pin.
Since the DAC is used for part of the ADC conversion process it would
seem unlikely that you could use it for other purposes. However, the DAC output has a track
and hold amplifier which holds the output constant
while DAC output is changed to a different value.
So in fact you can use the DAC output at the same time as using the ADC inputs.
TIP: You can use the DAC analogue output (Aout) since it has a track and hold amplifier.
DAC Output Current
There is a slight problem with the DAC output and that is its current
output changes close to 0V or Vcc - so the impedance changes depending
on the output voltage.
The datasheet shows that the impedance
of the DAC output increases at the ends of the range, as shown below:
[Source: Datasheet]
This means that the closer you get to the rails the less current is
available hence the higher the impedance presented at the output.
In fact the datasheet gives an example of a load of 10k at the analogue
output, but the output voltage is then limited to 0.9x VDD. Note that for no-load, the output will go all the way to VDD - this means the output current from the DAC is not that good.
Warning: The DAC will not go to the positive supply rail (10k load).
The other problem is that you can't get much current out of it anyway
so buffering the DAC output using an opamp is a good idea.
The datasheet does not directly say how much current you can draw but
indicates when Aout is off, the supply current is 250uA. So there should
be 750uA to play with - that means do not load Aout by much, and
certainly a lot less than 750uA.
You can use any Arduino with this device, as the chip operates from 2.5
to 6.0 volts. Here an Arduino Uno or Arduino Nano is used.
You simply connect the device to the I2C pins on your Arduino.
If you don't use the default I2C pins then Arduino software
will use a bit banged (slower) method to control the device.
This means you can have more than one I2C bus on a single Arduino board.
TIP: Use any pins on the Arduino as I2C, for extra I2C buses (slower).
...and install into the 'libraries' directory in your Arduino installation.
Instructions on how to do this are here - use the zip install or manual install method as it is not available through the libraries manager tool (Arduino IDE).
PCF8591 Functions
The library has functions for:
Initialisation.
ADC Read / Bulk Read.
DAC Write.
Chip shutdown.
One useful feature is the power_save argument. This turns off the Chip after every ADC read, saving power.
There are two types of read operation:
raw.
adjusted (using a voltage reference parameter that you supply).
PCF8591 Power save
Power save mode is entered when you initialise the object:
PCF8591( uint8_t addr, bool power_save );
PCF8591 ADC Read Raw
uint8_t adc_raw_read( uint8_t channel );
The corresponding bulk read expects you to supply a 4 x 8bit array pointer:
void adc_bulk_raw_read( uint8_t* res );
PCF8591 ADC Read
These functions mirror raw versions but require a voltage reference
value. They work by calculating a floating point conversion relative to
the reference value:
double adc_read( uint8_t port, double v_ref );
The corresponding bulk read expects you to supply a 4 x 8bit array pointer:
Note: The variable '_power_save' is initialised in function PCF8591() so it does not need to be initialised in the header file.
Simple Arduino Sketch for PCF8591
This example outputs the raw ADC values to the serial terminal, for
each analogue input. On the breakout board these are connected to the
LDR (Light Dependent Resistor),
the thermistor and the potentiometer.
A2 (AIN2) is not connected to any on board circuit i.e. it is
floating and can be used to accept an external voltage for measurement.
To use all the inputs without any on board circuitry attached just
remove all the jumpers.
In the program the potentiometer ADC reading is output to the Analogue output Aout.
So changing the POT value will control the voltage sent to D1 (Green) and change its brightness.
Remember to wait for the program loop to update as it is set to include a delay of 1 second i.e.
changes won't be apparent until after a second has passed!
Warning: The D1 LED and 1k Resistor presents a large load to the Aout pin (See here for details).
so for correct usage unsolder either D1 or R3 to stop this load if you want to have proper analogue out operation.
Even then you should use an opamp buffer.
#include <Wire.h>
#include "PCF8591.h" // https://github.com/overbog/PCF8591
PCF8591pcf8591(0x48,0);
////////////////////////////////////////////////////////////////
voidsetup(){
Wire.begin();
Serial.begin(115200);// initialize serial communication
pcf8591.begin();
}
////////////////////////////////////////////////////////////////
// Right align a value (type long) for column algined text output.
// For ADC can get large numbers and int too small.
voidprintLongRight(bytefieldSize,longv){
charstr[10];
ltoa(v,str,10);
intlen=strlen(str);
if(len>fieldSize)len=fieldSize;
intspc=fieldSize-len;
for(inti=0;i<spc;i++)Serial.print(' ');
for(inti=0;i<len;i++){
Serial.print(str[i]);
}
}
////////////////////////////////////////////////////////////////
voidloop(){
uint8_tres[4];
intana0V=pcf8591.adc_raw_read(0);
intana1V=pcf8591.adc_raw_read(1);
intana2V=pcf8591.adc_raw_read(2);
intana3V=pcf8591.adc_raw_read(3);
Serial.print("A0 BRIGHT ");printLongRight(6,ana0V);
Serial.print(" A1 TEMP ");printLongRight(6,ana1V);
Serial.print(" A2 ANA ");printLongRight(6,ana2V);
Serial.print(" A3 POT ");printLongRight(6,ana3V);
Serial.println();
pcf8591.adc_bulk_raw_read(res);
Serial.print("A3 raw ");Serial.println(res[3]);
Serial.print("Read VCC (assumed Vref of 5V): ");
Serial.println(pcf8591.adc_read(3,5));
// Write the ADC value from the pot to Aout.
pcf8591.dac_write(pcf8591.adc_raw_read(3));
delay(1000);
}
[pcf8591.ino]
PCF8591 Operational Analysis
To find out a bit more about the performance I added timers to the above
program to see how fast the chip performs (See the sketch below). Quite surprisingly the
chip under-performs by a significant amount. There are hints in
the datasheet that suggest this performance limitation:
Conversion Speed
Important texts from the datasheet:
Section: Features and Benefits
"Max sampling rate given by I2C-bus speed"
Section: 8.4 A/D Conversion
"The maximum A/D conversion rate is given by the actual speed of the I2C-bus."
I2C Transaction Operation
Section 8.4 A/D conversion
"
"
The last paragraph above shows the problem :
"An A/D conversion cycle is always
started after sending a valid read mode
address to a PCF8591 device."
That means conversions are tied directly to the I2C bus timing and yes "Max sampling rate given by I2C-bus speed" is therefore absolutely true.
The implication of this statement is that the sampling rate is not just "given by" but "limited by" the I2C sampling rate. The I2C rate determines how fast you can read the chip.
Two I2C transactions for a read
It also means this device must wait for a complete IC transaction before it will start
an A/D conversion. In addition to that, the output results are always
delayed by one I2C transaction since the second statement states :
"The A/D conversion cycle is triggered at the trailing edge of the
acknowledge clock pulse and is
executed while transmitting the result of the
previous conversion."
That means to get a single up-to-date ADC reading you make a read, then
throw the result away, and then do a second ADC read and keep that!
PCF8591 Misleading Datasheet
You may think (as I did) that the sampling frequency 11.1kHz (or
conversion time period - 90us) gives you a good indication of the
speed performance of the chip.
It does not!!!!
The over-riding parameter that determines the speed of operation of this device
is the I2C bus and that is limited to 100kHz. 100kHz sounds good you
say:
It sounds good, but there are three relevant facts:
You must send an address 1st.
You must perform 2 reads to get one out (see above).
100kHz is quite slow when you want to transfer data fast (1/100k = 10us).
PCF8591 Timing analysis
The key stumbling block in this device is the speed of the I2C interface.
When you add up I2C byte transactions time is consumed.
Lets assume that the above operation takes 3 x I2C packets; This is 8
data bits plus an ACK, START and STOP. Lets also assume a turn-around time
for the processor of ~3 bits (time equivalent to 3bits).
This is a guesstimate so not absolutely accurate, but will give a ball-park estimation. So we need to do an I2C transaction of:
3*(Bytes+ACK+START+STOP) + 3*processor time = 3*11+3*3 = 42 bits
This is 42 bits before we see any data from the device. The time to do this at 100kHz I2C rate is:
42*(1/100e3) = 0.42ms , 1.0/0.42e-3 = 2380Hz
So if you think you are going to get a sample rate (out of the chip of
11kHz) - think again! In fact the measured results are worse than this (See tests below)!
Sketch for Timing Test
#include <Wire.h>
#include "PCF8591.h" // https://github.com/overbog/PCF8591
PCF8591pcf8591(0x48,false);// ADDR, power_save
PCF8591pcf8591_power_save(0x48,true);// ADDR, power_save
////////////////////////////////////////////////////////////////
voidsetup(){
Wire.begin();
Serial.begin(115200);// initialize serial communication
pcf8591.begin();
pcf8591_power_save.begin();
}
////////////////////////////////////////////////////////////////
// Right align a value (type long) for column algined text output.
// For ADC can get large numbers and int too small.
voidprintLongRight(bytefieldSize,longv){
charstr[10];
ltoa(v,str,10);
intlen=strlen(str);
if(len>fieldSize)len=fieldSize;
intspc=fieldSize-len;
for(inti=0;i<spc;i++)Serial.print(' ');
for(inti=0;i<len;i++){
Serial.print(str[i]);
}
}
////////////////////////////////////////////////////////////////
// Takes a pointer to the object as power save set at initialisation.
voidtest_read_time(PCF8591*ptr){
uint8_tres[4];
Serial.println("~~~~~~~~~~~~~~");
unsignedlongtimeMicro=micros();
for(inti=0;i<10;i++)
ptr->adc_bulk_raw_read(res);
timeMicro=micros()-timeMicro;
Serial.print("Block read takes : ");
Serial.print(timeMicro/10);
Serial.println(" micro seconds");
Serial.print("Frequency : ");
floatfval=1.0/(((float)timeMicro/10.0)*1e-6);// 1.0/((float)timeMicro/10.0
Serial.print(fval);
Serial.println("Hz");
Serial.println("~~~~~~~~~~~~~~");
Serial.println("~~~~~~~~~~~~~~");
timeMicro=micros();
for(inti=0;i<10;i++)
ptr->adc_raw_read(2);
timeMicro=micros()-timeMicro;
Serial.print("Single read takes : ");
Serial.print(timeMicro/10);
Serial.println(" micro seconds");
Serial.print("Frequency : ");
fval=1.0/(((float)timeMicro/10.0)*1e-6);// 1.0/((float)timeMicro/10.0
Serial.print(fval);
Serial.println("Hz");
Serial.println("~~~~~~~~~~~~~~");
}
////////////////////////////////////////////////////////////////
voidloop(){
intana0V=pcf8591.adc_raw_read(0);
intana1V=pcf8591.adc_raw_read(1);
intana2V=pcf8591.adc_raw_read(2);
intana3V=pcf8591.adc_raw_read(3);
Serial.print("A0 BRIGHT ");printLongRight(6,ana0V);
Serial.print(" A1 TEMP ");printLongRight(6,ana1V);
Serial.print(" A2 ANA ");printLongRight(6,ana2V);
Serial.print(" A3 POT ");printLongRight(6,ana3V);
Serial.println();
Serial.println("####################################");
Serial.println("HIGH POWER MODE OSCILLATOR ON:");
pcf8591.dac_write(127);// Enable dac which forces oscillator on.
test_read_time(&pcf8591);
Serial.println("####################################");
Serial.println("LOW_POWER MODE WAIT FOR OSCILLATOR:");
test_read_time(&pcf8591_power_save);
Serial.print("Read VCC (assumed Vref of 5V): ");
Serial.println(pcf8591.adc_read(3,5));
// Write the ADC value from the pot to Aout.
pcf8591.dac_write(pcf8591.adc_raw_read(3));
delay(1000);
}
[pcf8591_timing_test.ino]
Test results for Timing Test
####################################
HIGH POWER MODE OSCILLATOR ON:
~~~~~~~~~~~~~~
Block read takes : 951 micro seconds
Frequency : 1050.86Hz
~~~~~~~~~~~~~~
~~~~~~~~~~~~~~
Single read takes : 665 micro seconds
Frequency : 1503.31Hz
~~~~~~~~~~~~~~
####################################
LOW_POWER MODE WAIT FOR OSCILLATOR:
~~~~~~~~~~~~~~
Block read takes : 1180 micro seconds
Frequency : 847.46Hz
~~~~~~~~~~~~~~
~~~~~~~~~~~~~~
Single read takes : 888 micro seconds
Frequency : 1125.62Hz
~~~~~~~~~~~~~~
Read VCC (assumed Vref of 5V): 2.80
A0 BRIGHT 220 A1 TEMP 214 A2 ANA 178 A3 POT 143
Note: These results are PCF8591 specific so they also apply to using the PCF8591 on the Raspberry PI.
Timing Analysis Conclusions
The sample rate is at best 1.5kHz but for bulk reads, lowers to 1kHz.
So it is about 10 times lower than the datasheet leads you to expect
(11kHz).
TIP: If you really need to get this
chip working faster then think about the double read requirement - it
may be possible to get continuous reads from the device. Check the datasheet and experiment for this. See Fig. 16 in datasheet.
The sample rate is even lower for 'low power mode' because of a delay
written into the library code. The delay is a 100us delay to allow the
chip to start up after power off, but that delay is not
defined in the datasheet.
Warning: The sample rate is ~10 times lower than expected.
PCF8591 Conclusions
The PCF8591 ADC is not the fastest and does not have large resolution.
It is useful you need some extra analogue inputs that can eliminate noise (using pairs of inputs in differential mode).
The Sample rate is low
The fastest sample rate you are likely to get out of the PCF8591 is
1.5kHz (assuming the rest of the code is in loop() - and is not doing very
much!). For reading all four ADC inputs at once the sample rate reduces
to 1kHz. The sample rate is controlled by the maximum I2C speed and for
this chip it is specified as 100kHz.
The DAC is limited
The ADC can not go greater than 0.9Vcc for a 10k load. This is quite a
low load so is disappointing. In addition at the extremes of voltage output the DAC
output reduces current i.e. it gets worse in terms of impedance. However it is better than having no DAC at all!
Cost
For a project that does not require high speed operation but lots of
analogue inputs it may be a good choice if bought in bulk since it will be lower cost than more capable chips. It has to be
for a project that only needs a low sample rate, low resolution (8
bit) and requires lots of analogue inputs (up to 32).
Low noise
One very good feature is that since pairs of analogue inputs can be configured as differential inputs
you can easily eliminate noise voltages when taking a reading.
Unlock the secrets of Arduino scrolling displays! This beginner-friendly guide shows you how to create real-time, dynamic graphics using an SSD1306 OLED, perfect for tracking sensor data and building…
How to get accurate DHT22 digital humidity sensor readings with an Arduino. Did you know it also measures temperature as Well? Find out why, in this page...
A PIR sensor lets your Arduino sense movement without contact. This tutorial covers PIR sensor basics, connecting one to an Arduino board and coding a motion detector.
Arduino Hall Effect Sensor: Add magnetic sensing superpowers to your Arduino projects with an easy-to-use hall effect sensor. With full code and layout...
Comments
Have your say about what you just read! Leave me a comment in the box below.
Don’t see the comments box? Log in to your Facebook account, give Facebook consent, then return to this page and refresh it.