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).
PCF8591:

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.

PCF8591 Breakout board

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.

PCF8591 Specification

  Parameter
PCF8591
Voltage Supply (Vs)
 2V5 ~ 6V0
Abs. Max VDD
-0V5 ~ 8V0
Interface
I2C
I2C rate
100kHz
Resolution (ADC)
8 bit
Inputs (Single ended / differential)
4 / 2
Supply current max (Aout off, Aout on)
250uA / 1mA
Standby mode (no load - typ,max)
1uA, 15uA
Offset error (ADC, DAC)
20mV, 50mV
Linearity error
±1.5LSB
Gain error
1%
Conversion time
90us [see here]
Sampling frequency
11.1kHz [see here]
Differential input range
-Vfs/2 ~ +Vfs/2
I2C Addresses (h/w selected = 8off)
0x48 ~ 0x4F
Operating temperature
-40°C ~ 85°C

PCF8591 Datasheet

Download the PCF8591 datasheet here.

PCF8591 Pinout

The pinout for the DIP version and SO16 (SMD) are identical:

PCF8591 Pinout
                                            [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.

pcf8591 analogue inputs: single ended or differential
                                             [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:

DAC output impedance near the rails

           [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.

Hardware

PCF8591 YL-40 Schematic of Breakout Board

PCF8591 breakout board schematic

View larger image here.

Yl-40 Component location

Breakout board PCF8591

Arduino Board

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).

Software

PCF8591 Library

Download and install the zip library here:

    https://github.com/overbog/PCF8591

...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:

    void adc_bulk_read( double* res, double v_ref );

TIP: TIP: Find how to use the Arduino Internal Voltage Reference to increase the accuracy of the ADC reading, in the link.

PCF8591 DAC write

    void dac_write( uint8_t value );

PCF8591 Shutdown

    void dac_shutdown();

This command shuts down the chip oscillator which saves power.

Code Warning

Library Power Down Operation

There are two edits (below) to the library that correct the dac powerdown operation:

Library edit One

The library does not initialise _dac_enable so it is undetermined at run time. Therefore edit the code in PCF8591.h from:


    bool _dac_enable;

to:

    bool _dac_enable = false;

...to ensure correct power down operation.

Library edit Two

You also need to edit the dac_shutdown function (in PCF8591.cpp)  to include the _dac_enable setting as shown below:


    void PCF8591::dac_shutdown()
    {
        _dac_enable = false;
        Wire.beginTransmission( _i2c_addr );
        Wire.write( 0 );
        Wire.endTransmission();
    }

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

PCF8591 pcf8591(0x48,0);

////////////////////////////////////////////////////////////////
void setup() {
    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.
void printLongRight(byte fieldSize, long v) {
char str[10];
   ltoa(v,str,10);
   int len = strlen(str);
   if (len>fieldSize) len = fieldSize;
   int spc = fieldSize-len;
   for( int i=0;i<spc;i++) Serial.print(' ');
   for( int i=0;i<len;i++) {
      Serial.print(str[i]);
   }
}

////////////////////////////////////////////////////////////////
void loop() {

   uint8_t res[4];

   int ana0V = pcf8591.adc_raw_read(0);
   int ana1V = pcf8591.adc_raw_read(1);
   int ana2V = pcf8591.adc_raw_read(2);
   int ana3V = 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

        "
        PCF8591 section8-4
        "

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:

  1. You must send an address 1st.
  2. You must perform 2 reads to get one out (see above).
  3. 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

PCF8591 pcf8591(0x48,false);              // ADDR, power_save
PCF8591 pcf8591_power_save( 0x48, true ); // ADDR, power_save

////////////////////////////////////////////////////////////////
void setup() {
    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.
void printLongRight(byte fieldSize, long v) {
char str[10];
   ltoa(v,str,10);
   int len = strlen(str);
   if (len>fieldSize) len = fieldSize;
   int spc = fieldSize-len;
   for( int i=0;i<spc;i++) Serial.print(' ');
   for( int i=0;i<len;i++) {
      Serial.print(str[i]);
   }
}
////////////////////////////////////////////////////////////////
// Takes a pointer to the object as power save set at initialisation.
void test_read_time(PCF8591 *ptr) {
uint8_t res[4];

   Serial.println("~~~~~~~~~~~~~~");
   unsigned long timeMicro = micros();
   for (int i=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 : ");
   float fval =   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 (int i=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("~~~~~~~~~~~~~~");
}

////////////////////////////////////////////////////////////////
void loop() {

   int ana0V = pcf8591.adc_raw_read(0);
   int ana1V = pcf8591.adc_raw_read(1);
   int ana2V = pcf8591.adc_raw_read(2);
   int ana3V = 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.




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.




Privacy Policy | Contact | About Me

Site Map | Terms of Use