PIC C, PCF8591 A/D and D/A


Introduction.

The Philips PCF8591 is a four channel 8-bit A/Ds and one 8-bit D/A. The four A/Ds may be configured as single ended or differential or a combination of the two. An external V_REF is provided. A data sheet in .pdf format may be obtained from http://www.philips.com. We sell the device in a DIP package.

The device uses the Philips I2C protocol. The manufacturer's device code is 1001. The PCF8591 provides three additional address terminals A2, A1 and A0.

Three routines are presented. The first illustrates how to write to the D/A. The second illustrates how one might perform waveform synthesis. One aspect of this routine is to illustrate how one might implement a two dimensional array. The third illustrates how to perform four single ended A/D measurements.

Sample Programs.

8591_1.C

In program 8591_1.C a counter is incremented from 0x00 to 0xff which then rolls over to 0x00 and this counter is output so as to generate a sawtooth on the D/A output.

The sequence in outputting data is to first address the device followed by the configuration data, followed by the data itself. In this case the configuration data is simply to set bit 6 to a one to enable the D/A output.

Note that the LCD routines were included only to aid in the debugging process.

// Program 8591_1.C, CCS PCM, PIC16F84
//
// Illustrates how to use PCF8591 Digital to Analog Capability.
//
// Program continually loops, each time incrementing a counter n
// and outputting the counter value to the D/A.  The result is a 
// sawtooth.
//
//    PIC16F84				PCF8591
//
// RB1 (term 7) ------------------- SCL (term 10) ----- To Other
// RB2 (term 8) ------------------- SDA (term 9) ----- I2C Devices
//
// Note that the slave address is determined by A2 (term 7), A1
// (term 6) and A0 (term 5) on the PCF8591.  The above SCL and SDA leads
// may be multipled to eight group "1001" devices, each strapped for a
// unique A2 A1 A0 setting.  In this example, A2, A1 and A0 are strapped
// for 0, 0, 0.
//
// 10K pullup resistors to +5VDC are required on both SDA and SCL signal 
// leads.
//
// Note that EXT (terminal 12) on 8591 is at ground (internal OSC).
// D_A output is at 8591 terminal 15.
//
// copyright, Peter H. Anderson, Baltimore, MD, May, '99

#case
#include <16f84.h>
#include <defs_f84.h>

void _8591_d_a(int dev_adr, int dat);	// output dat to D/A
// dev_adr is a2, a1, a0 strapping

// common i2c routines
byte i2c_in_byte(void);
void i2c_out_byte(byte o_byte);
void i2c_nack(void);
void i2c_ack(void);
void i2c_start(void);
void i2c_stop(void);
void i2c_high_sda(void);
void i2c_low_sda(void);
void i2c_high_scl(void);
void i2c_low_scl(void);

// delay routines
void delay_ms(long t);
void delay_10us(int t);

// LCD routines - used only for debugging
void lcd_init(void);
void out_RAM_str(int *s);
void lcd_hex_byte(int val);
void lcd_dec_byte(int val, int digits);
int num_to_char(int val);
void lcd_char(int ch);
void lcd_new_line(void);

#define TxData 0	// RA.0 for serial LCD

#define SDA_PIN rb2	// RB.2
#define SCL_PIN rb1	// RB.1

#define SDA_DIR trisb2
#define SCL_DIR trisb1

void main(void)
{
   int n=0;
   while(1)	// continually generate a sawtooth
   {
      _8591_d_a(0x00, n);
      //delay_10us(5);
      ++n;
   }
}

void _8591_d_a(int dev_adr, int dat)
{
   i2c_start();
   i2c_out_byte(0x90 | (dev_adr<<1));	// address the device
   i2c_nack();
   i2c_out_byte(0x40); 	// config to enable D/A output
   i2c_nack();
   i2c_out_byte(dat);	// D/A data
   i2c_nack();
   i2c_stop();
}

// Common I2C Routines

byte i2c_in_byte(void)
{
   byte i_byte, n;
   i2c_high_sda();
   for (n=0; n<8; n++)
   {
      i2c_high_scl();

      if (SDA_PIN)
      {
         i_byte = (i_byte << 1) | 0x01; // msbit first
      }
      else
      {
         i_byte = i_byte << 1;
      }
      i2c_low_scl();
   }
   return(i_byte);
}

void i2c_out_byte(byte o_byte)
{
   byte n;
   for(n=0; n<8; n++)
   {
      if(o_byte&0x80)
      {
         i2c_high_sda();
      }
      else
      {
         i2c_low_sda();
      }
      i2c_high_scl();
      i2c_low_scl();
      o_byte = o_byte << 1;
   }
   i2c_high_sda();
}

void i2c_nack(void)
{
   i2c_high_sda();	// data at one
   i2c_high_scl();	// clock pulse
   i2c_low_scl();
}

void i2c_ack(void)
{
   i2c_low_sda();	// bring data low and clock
   i2c_high_scl();
   i2c_low_scl();
   i2c_high_sda();
}


void i2c_start(void)
{
   i2c_low_scl();
   i2c_high_sda();
   i2c_high_scl();	// bring SDA low while SCL is high
   i2c_low_sda();
   i2c_low_scl();
}

void i2c_stop(void)
{
   i2c_low_scl();
   i2c_low_sda();
   i2c_high_scl();
   i2c_high_sda();  // bring SDA high while SCL is high
   // idle is SDA high and SCL high
}

void i2c_high_sda(void)
{
   // bring SDA to high impedance
   SDA_DIR = 1;
   // delay_10us(5);
}

void i2c_low_sda(void)
{
   SDA_PIN = 0;
   SDA_DIR = 0;  // output a hard logic zero
   // delay_10us(5);
}

void i2c_high_scl(void)
{
   SCL_DIR = 1;   // high impedance
   // delay_10us(5);
}

void i2c_low_scl(void)
{
   SCL_PIN = 0;
   SCL_DIR = 0;
   // delay_10us(5);
}

// delay routines

void delay_10us(int t)
{
#asm
      BCF STATUS, RP0
DELAY_10US_1:
      CLRWDT
      NOP
      NOP
      NOP
      NOP
      NOP
      NOP
      DECFSZ t, F
      GOTO DELAY_10US_1
#endasm
}

void delay_ms(long t)	// delays t millisecs
{
   do
   {
     delay_10us(100);
   } while(--t);
}

// LCD routines
int num_to_char(int val)	// converts val to hex character
{
   int ch;
   if (val < 10)
   {
     ch=val+'0';
   }
   else
   {
     val=val-10;
     ch=val + 'A';
   }
   return(ch);
}

void lcd_char(int ch)	// serial output to PIC-n-LCD, 9600 baud
{
   int n, dly;
   		// start bit + 8 data bits

#asm
       BCF STATUS, RP0
       MOVLW 9
       MOVWF n
       BCF STATUS, C

LCD_CHAR_1:

       BTFSS STATUS, C
       BSF PORTA, TxData
       BTFSC STATUS, C
       BCF PORTA, TxData
       MOVLW 32
       MOVWF dly

LCD_CHAR_2:
       DECFSZ dly, F
       GOTO LCD_CHAR_2
       RRF ch, F
       DECFSZ n, F
       GOTO LCD_CHAR_1

       BCF PORTA, TxData
       CLRWDT
       MOVLW 96
       MOVWF dly

LCD_CHAR_3:
       DECFSZ dly, F
       GOTO LCD_CHAR_3
       CLRWDT
#endasm
}

void lcd_init(void)	// sets TxData in idle state and resets PIC-n-LCD
{
#asm
        BCF STATUS, RP0
        BCF PORTA, TxData
        BSF STATUS, RP0
        BCF TRISA, TxData
        BCF STATUS, RP0
#endasm
   lcd_char(0x0c);
   delay_ms(250);
}

void lcd_new_line(void)	// outputs 0x0d, 0x0a
{
   lcd_char(0x0d);
   delay_ms(10);	// give the PIC-n-LCD time to perform the
   lcd_char(0x0a);	// new line function
   delay_ms(10);
}


void out_RAM_str(int *s)
{
   while(*s)
   {
      lcd_char(*s);
      ++s;
   }
}

void lcd_hex_byte(int val) // displays val in hex format
{
   int ch;
   ch = num_to_char((val>>4) & 0x0f);
   lcd_char(ch);
   ch = num_to_char(val&0x0f);
   lcd_char(ch);
}

void lcd_dec_byte(int val, int digits)
// displays byte in decimal as either 1, 2 or 3 digits
{
   int d;
   int ch;
   if (digits == 3)
   {
      d=val/100;
      ch=num_to_char(d);
      lcd_char(ch);
   }
   if (digits >1)	// take the two lowest digits
   {
       val=val%100;
       d=val/10;
       ch=num_to_char(d);
       lcd_char(ch);
   }
   if (digits == 1)	// take the least significant digit
   {
       val = val%100;
   }

   d=val % 10;
   ch=num_to_char(d);
   lcd_char(ch);
}


Program 8591_2.C

This program illustrates how the D/A might be used for waveform synthesis. PIC inputs RB.7 and RB.6 are read to determine the waveform and RB.5 and RB.4 to determine the delay between each output.

Each of the four waveforms consists of 16 samples. As the CCS compiler does not support two dimensional arrays, we used a 64 byte array. The 16 samples which are output is then calculated as wave_index*16 + n, where n is incremented from 0 to 15.


// Program 8591_2.C
//
// Illustrates how to use PCF8591 Digital to Analog capability for
// waveform synthesis
//
// Program continually loops, each time checking inputs RB.7 and RB.6
// which determine the waveform to be synthesized.
//
//	RB7	RB6
//	0	0	Rising Sawtooth
//	0	1	Falling Sawtooth
//	1	0	Triangular
//	1	1	Square Wave
//
// The delay between D/A outputs is determined by inputs at RB.5 and
// RB.4
//
//    PIC16F84				PCF8591
//
// RB1 (term 7) ------------------- SCL (term 10) ----- To Other
// RB2 (term 8) ------------------- SDA (term 9) ----- I2C Devices
//
// 	------ RB7 (term 13)	waveform select
//	------ RB6 (term 12)
//
//	------ RB5 (term 11)	speed select
//	------ RB4 (term 10)
//

// Note that the I2C slave address is determined by A2 (term 7), A1
// (term 6) and A0 (term 5) on the PCF8591.  The above SCL and SDA leads
// may be multipled to eight group "1001" devices, each strapped for a
// unique A2 A1 A0 setting.
//
// 10K pullup resistors to +5VDC are required on both signal leads.
//
// Note that EXT (terminal 12) on 8591 is at ground (internal OSC).
// D_A output is at 8591 terminal 15.
//
// copyright, Peter H. Anderson, Baltimore, MD, May, '99

#case
#include <16f84.h>
#include <defs_f84.h>

void _8591_d_a(int dev_adr, int dat);

// common i2c routines
byte i2c_in_byte(void);
void i2c_out_byte(byte o_byte);
void i2c_nack(void);
void i2c_ack(void);
void i2c_start(void);
void i2c_stop(void);
void i2c_high_sda(void);
void i2c_low_sda(void);
void i2c_high_scl(void);
void i2c_low_scl(void);

// delay routines
void delay_ms(long t);
void delay_10us(int t);

// LCD routines
void lcd_init(void);
void out_RAM_str(int *s);
void lcd_hex_byte(int val);
void lcd_dec_byte(int val, int digits);
int num_to_char(int val);
void lcd_char(int ch);
void lcd_new_line(void);

#define TxData 0	// RA.0 for serial LCD

#define SDA_PIN rb2	// RB.2
#define SCL_PIN rb1	// RB.1

#define SDA_DIR trisb2
#define SCL_DIR trisb1

void main(void)
{
   int  delay_index, wave_index, n;
   int const delay[4] = {10, 20, 50, 80};
   int const wave[64] =

    // rising sawtooth
       {0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70,
         0x80, 0x90, 0xa0, 0xb0, 0xc0, 0xd0, 0xe0, 0xf0,

    // falling sawtooth

        0xf0, 0xe0, 0xd0, 0xc0, 0xb0, 0xa0, 0x90, 0x80,
         0x70, 0x60, 0x50, 0x40, 0x30, 0x20, 0x10, 0x00,

    // triangular

        0x00, 0x20, 0x40, 0x60, 0x80, 0xa0, 0xc0, 0xe0,
         0xe0, 0xc0, 0xa0, 0x80, 0x60, 0x40, 0x20, 0x00,

   // square wave

	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
         0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
   not_rbpu=0;	// enable weak pullups
   while(1)
   {
      TRISB=0xff;  // all of portb are inputs
      delay_index = (rb5<<1) | rb4;
      wave_index=(rb7<<1) | rb6;

      for(n=0; n<16; n++)	// 16 samples
      {
         _8591_d_a(0x00, wave[wave_index*16 + n]);
         delay_10us(delay[delay_index]);
      }
   }
}

void _8591_d_a(int dev_adr, int dat)
{
   i2c_start();
   i2c_out_byte(0x90 | (dev_adr<<1));
   i2c_nack();
   i2c_out_byte(0x40); // config to perform D/A
   i2c_nack();
   i2c_out_byte(dat);
   i2c_nack();
   i2c_stop();
}

// Common I2C Routines
// same as Program 8591_1.C

// Delay Routines
// same as 8591_1.C

// LCD Routines - used for debugging only
// same as 8591_1.C


Program PCF8591_3.C

This program configures the four A/D converters as single ended and continually reads each of the four A/Ds and displays the results on a serial LCD.

Note that the configuration register is configured for autoincrement, beginning at A/D 0. Note that the first byte which is read is thrown away. I assume the reason for this is that the clock pulses used inputting this "dummy" byte are used by the PCF8591 to perform the successive approximation A/D conversion on Ch0, and the eight clock pulses used in inputting the A/D 0 result are used to similarly perfrom the successive approximation on Ch1, etc.

// Program 8591_3.C
//
// Illustrates how to use PCF8591 Analog to Digital Capability.
//
// Program continually loops, each time performing a series of single
// ended measurements on each of the four analog channels which are 
// displayed on the serial LCD on RA.0.
//
//    PIC16F84				PCF8591
//
// RB1 (term 7) ------------------- SCL (term 10) ----- To Other
// RB2 (term 8) ------------------- SDA (term 9) ----- I2C Devices
//
// Note that the slave address is determined by A2 (term 7), A1
// (term 6) and A0 (term 5) on the PCF8591.  The above SCL and SDA leads
// may be multipled to eight group "1001" devices, each strapped for a
// unique A2 A1 A0 setting.
//
// 10K pullup resistors to +5VDC are required on both signal leads.
//
// Note that EXT (terminal 12) on 8591 is at ground (internal OSC).
//
// copyright, Peter H. Anderson, Baltimore, MD, May, '99

#case
#include <16f84.h>
#include <defs_f84.h>

void _8591_ad_meas(int dev_adr, int *d);

// common i2c routines
byte i2c_in_byte(void);
void i2c_out_byte(byte o_byte);
void i2c_nack(void);
void i2c_ack(void);
void i2c_start(void);
void i2c_stop(void);
void i2c_high_sda(void);
void i2c_low_sda(void);
void i2c_high_scl(void);
void i2c_low_scl(void);

// delay routines
void delay_ms(long t);
void delay_10us(int t);

// LCD routines
void lcd_init(void);
void out_RAM_str(int *s);
void lcd_hex_byte(int val);
void lcd_dec_byte(int val, int digits);
int num_to_char(int val);
void lcd_char(int ch);
void lcd_new_line(void);

#define TxData 0	// RA.0 for serial LCD

#define SDA_PIN rb2	// RB.2
#define SCL_PIN rb1	// RB.1

#define SDA_DIR trisb2
#define SCL_DIR trisb1

void main(void)
{
   int d[4], n;
   lcd_init();
   while(1)
   {
      _8591_ad_meas(0x00, d); // perform 4 single ended measurments
	// note that results are passed by reference

      for(n=0; n<4; n++)	// display the results on serial LCD
      {
         lcd_hex_byte(d[n]);
         lcd_char(' ');
      }
      lcd_new_line();
      delay_ms(1000);  // one second
   }
}

void _8591_ad_meas(int dev_adr, int *d)
{
   int n;

   // set configuration
   i2c_start();
   i2c_out_byte(0x90 | (dev_adr<<1));
   i2c_nack();
   i2c_out_byte(0x44);	// internal osc on, autoincrement,
			// 4-single ended measurments
   i2c_nack();
   i2c_out_byte(0x00);	// start with A/D 0
   i2c_nack();
   i2c_stop();		

   // read result
   i2c_start();
   i2c_out_byte(0x91 | (dev_adr<<1));
   i2c_nack();

   i2c_in_byte();  // throw the first away
   i2c_ack();

   for(n=0; n<4; n++)
   {
      d[n]=i2c_in_byte();
      if(n!=3)		// no ack after fetching the last byte
      {
         i2c_ack();
      }
   }
   i2c_stop();
}

// Common I2C Routines
// same as Program 8591_1.C

// Delay Routines
// same as 8591_1.C

// LCD Routines
// same as 8591_1.C