Waveform Synthesis using a Philips PCF8591 4-Ch A/D & D/A

copyright, Nicole Ambrose, Dept of Electrical Engineering,
Morgan State University, Baltimore, MD 21239, July 12, '97

Introduction.

This discussion focuses on the use of a Philips PCF8591 A/D and D/A converter for synthesizing waveforms.

The PCF8591 uses the 2-wire Philips I2C protocol and up to eight such devices may be cascaded on the same two signal leads. It is available from Newark Electronics http://www.newark.com for nominally $5.00.

Philip's WWW page at http://www.semiconductors.philips.com is probably the worst I have seen of any major integrated circuit manufacturer and in my searches of their page I have not been able to find a data sheet for this powerful device. I sent e-mail to Philips and a local distributor sent me one. If all else fails, send me $1.00 to cover my copy costs and postage.

The Philips PCF8591 is a 4-channel 8-bit A/D which is capable of performing up to four single ended measurments. It may also be used in a number of different combinations of single ended and differential configurations. The A/D capabilities are discussed elsewhere.

D/A Capability.

This discussion focuses on the D/A capability of the PCF8591.

The device is capable of generating single analog output where the output is;

	V_out = V_REF * (D7/2 + D6/4 + D5/8 + D4/16 
                              + D3/32 + D2/64 + D1/128 + D0/256)

where D7 .. D0 are the values of the bits of an 8-bit value which is output to the D/A.

Program 8591_1.ASM. (Increasing Sawtooth).

Program 8591_1.ASM simply increments a value from 0 to FFH which then rolls over to 00H, etc, which is output to the D/A. The result is an output signal which is 0/256, 1/256, 2/256, ..., 255/256, 0/256, etc or an increasing sawtooth. The frequency is a function of the delay between outputting samples.

The interface is the Philips 2-wire I2C protocol. The low level I2C functions used in this program are discussed elsewhere in the context of a Philips PCF8574 8-bit I/O. Note that the PCF8591 has been assigned to a group address of 9 (1001) by the manufacturer. Thus, the address byte is;

	1 0 0 1 A2 A1 A0 R/W

Where R/W is zero for write operations and one for read operations.

(It is worthy of note that the DS1621 Digital Thermometer / Thermostat has the same manuafcturer's assigned group address. Thus, if DS1621 and PCF8591 devices are used on the same I2C bus, one must assure the A2, A1 A0 user assigned addresses are different.)

In routine D_A, D_A_VAL is output to the D/A.

This consists of sending the Start command, followed by the address byte with the R/W bit set to "write", followed by a NACK, followed by a byte which is written to the control register in the 8591, followed by another NACK, followed by the data to be output, followed by a NACK and finally the Stop command.

The control register provides a means to control the A/D configurations and the channel to be measured. In this discussion the only bit of interest is the Analog Output Flag (bit 6). When set to one, the analog output is turned on. Thus, in this program, the control register is set to %0100 0000 or 40H.

; Program 8591_1.ASM
;
; Illustrates how to use PCF8591 Digital to Analog Capability.
;
; Program continually loops, each time incrementing a counter D_A_VAL)
; and outputting the counter value to the D/A.  The result is a sawtooth.
;
;    PIC16C84				PCF8591
;
; RB7 (term 13) ------------------- SCL (term 10) ----- To Other
; RB6 (term 12) ------------------- 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).
; D_A output is at 8591 terminal 15.
;
; copyright, Nicole L. Ambrose, MSU, 11 July, '97

	LIST p=16c84
#include <c:\mplab\p16c84.inc>   
        __CONFIG 11h

	CONSTANT SDA=6
	CONSTANT SCL=7

	CONSTANT VARS=0CH

D_A_VAL	EQU VARS+0
DEV_ADR EQU VARS+1	; A2, A1, A0 

_N	EQU VARS+2	; used for I2C routines
O_BYTE	EQU VARS+3
I_BYTE	EQU VARS+4

LOOP1	EQU VARS+5	; timing
LOOP2	EQU VARS+6	; timing


	ORG 000H	

	BSF STATUS, RP0	; RP1=0, RPO=1, BANK1
        CLRF TRISB	; make all PORTB bits outputs
        BCF STATUS, RP0

	MOVLW .1	
	MOVWF DEV_ADR	; A2 A1 A0 address

	CLRF D_A_VAL	; for clarity
	
BEGIN:
	CALL D_A	; output value of N to D/A
	INCF D_A_VAL, F
	; insert delay as required.  1 msec used in this example
	MOVLW .1
	MOVWF LOOP1
	CALL DELAY_N_MS

	GOTO BEGIN

D_A:			; output D_A_VAL to PCF8591
	CALL START

	RRF DEV_ADR, W
	IORLW 90H
        MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK

        MOVLW 40H	; turn D/A output on
      	MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK  

        MOVF D_A_VAL, W	; output the D/A value
        MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK  

        CALL STOP
        RETURN

; The following routines are low level I2C routines applicable to most
; interfaces with I2C devices.

IN_BYTE				; read byte on i2c bus
	CLRF I_BYTE
	MOVLW .8
	MOVWF _N		; set index to 8	
	CALL HIGH_SDA		; be sure SDA is configured as input
IN_BIT
	CALL HIGH_SCL		; clock high
	BTFSS PORTB, SDA	; test SDA bit
	GOTO IN_ZERO
	GOTO IN_ONE

IN_ZERO
	BCF STATUS, C		; clear any carry
	RLF I_BYTE, F		; i_byte = i_byte << 1 | 0
	GOTO CONT_IN

IN_ONE
	BCF STATUS, C		; clear any carry
	RLF I_BYTE, F		
	INCF I_BYTE, F		; i_byte = (i_byte << 1) | 1
	GOTO CONT_IN

CONT_IN
	CALL LOW_SCL		; bring clock low
	DECFSZ _N, F		; decrement index
	GOTO IN_BIT
	RETURN

;;;;;;

OUT_BYTE:			; send o_byte on I2C bus
	MOVLW .8
	MOVWF _N
OUT_BIT:
	BCF STATUS,C		; clear carry
	RLF O_BYTE, F		; left shift, most sig bit is now in carry
	BTFSS STATUS, C		; if one, send a one
	GOTO OUT_ZERO
	GOTO OUT_ONE

OUT_ZERO:
	CALL LOW_SDA		; SDA at zero
	CALL CLOCK_PULSE	
	CALL HIGH_SDA
	GOTO OUT_CONT

OUT_ONE:
	CALL HIGH_SDA		; SDA at logic one
	CALL CLOCK_PULSE
	GOTO OUT_CONT

OUT_CONT:
	DECFSZ _N, F		; decrement index
	GOTO OUT_BIT
	RETURN	

;;;;;;
		
NACK:				; bring SDA high and clock
	CALL HIGH_SDA
	CALL CLOCK_PULSE
	RETURN

ACK:
	CALL LOW_SDA
	CALL CLOCK_PULSE
	RETURN

START:				
	CALL LOW_SCL
	CALL HIGH_SDA
	CALL HIGH_SCL
	CALL LOW_SDA		; bring SDA low while SCL is high
	CALL LOW_SCL
	RETURN

STOP:
	CALL LOW_SCL
	CALL LOW_SDA
	CALL HIGH_SCL
	CALL HIGH_SDA		; bring SDA high while SCL is high
	CALL LOW_SCL
	RETURN

CLOCK_PULSE:			; SCL momentarily to logic one
	CALL HIGH_SCL
	CALL LOW_SCL
	RETURN		

HIGH_SDA:			; high impedance by making SDA an input
	BSF STATUS, RP0	; bank 1
	BSF TRISB, SDA	; make SDA pin an input
	BCF STATUS, RP0	; back to bank 0
	CALL DELAY_SHORT
	RETURN

LOW_SDA:
	BCF PORTB, SDA	
	BSF STATUS, RP0	; bank 1
	BCF TRISB, SDA	; make SDA pin an output
	BCF STATUS, RP0	; back to bank 0
	CALL DELAY_SHORT
	RETURN

HIGH_SCL:
	BSF STATUS, RP0	; bank 1
	BSF TRISB, SCL	; make SCL pin an input
	BCF STATUS, RP0	; back to bank 0
	CALL DELAY_SHORT
	RETURN

LOW_SCL:
	BCF PORTB, SCL
	BSF STATUS, RP0	; bank 1
	BCF TRISB, SCL	; make SCL pin an output
	BCF STATUS, RP0	; back to bank 0
	CALL DELAY_SHORT
	RETURN

DELAY_SHORT:		; provides nominal 25 usec delay
	MOVLW .5
	MOVWF LOOP2
DELAY_SHORT_1:
	NOP
	DECFSZ LOOP2, F
	GOTO DELAY_SHORT_1
	RETURN 	

DELAY_LONG
	MOVLW .250	; 250 msec delay
	MOVWF LOOP1
DELAY_N_MS:		; delays value of msecs in LOOP1
OUTTER
	MOVLW	.110	; close to 1.0 msec delay when set to .110
	MOVWF 	LOOP2
INNER
	NOP
	NOP
	NOP
	NOP
	NOP
	NOP
	DECFSZ	LOOP2, F	; decrement and leave result in LOOP2 
				; skip next statement if zero
	GOTO INNER
	DECFSZ 	LOOP1, F
	GOTO OUTTER
	RETURN

	END

Program 8591_2.ASM. (Selectable Waveform and Delay).

This program continually reads "switches" on four PORTB bits configured as inputs. The settings of PORTB bits 1 and 0 determine the type of waveform and the settings of bits 3 and 2 determine the delay between outputting sequential samples.

The wave select bits 1 and 0 are read and isolated by anding with 03H. This is then multiplied by 16 decimal. Thus, the setting of the switches, results in a value of 00H, 10H, 20H or 30H. The value of INDEX which ranges from 0 to 15 is added to this.

The reader's attention is directed to the table WAVE_LOOKUP where four different waveforms, each consisting of 16 decimal bytes are stored.

Thus, if the two waveform switches are set to 0 and 1, a base value of 10H is calculated as described. Assumming INDEX is 0, the call to WAVE_LOOKUP causes sample[0] of a falling sawtooth to be returned and this is ouput to the D/A using the same D_A routine described above.

On the next pass, assumming the switches remain the same, the value of INDEX is now 1 and the call to WAVE_LOOKUP causes sample[1] to be returned, which is then output. This process continues through all 16 samples associated with the waveform identified by the two waveform select switches, and it is then repeated.

If the switches are changed, the based is changed. Thus, assume sample[5] of a falling sawtooth is output and INDEX is now incremented to 6. Assume that during this time, the two waveform select switches have been changed to 1 1, corresponding to a squarewave. The next output to the A/D will be sample[6] of a square wave. Assumming the waveform switches remain in the same position for some time, sample[7], sample[8], ..., sample[15], sample[0] of the square wave will be output.

One might think of this table as a two dimensional array where each element may be uniquely identified as;

	WAVE_SAMP[SW][INDEX]

where SW is the setting of the two waveform select switches.

Note that on each pass, PORTB bits 3 and 2 are also read to determine the delay between samples. These two bits are isolated and mapped into a value in the range of 0 - 3 by first isolating the two bits and perfoming two right shifts. This value is then mapped into the actual number of milliseconds using table DELAY_LOOKUP.

Note that the four switches which control the delay and the waveform consist of either grounding the input terminal or leaving it open. The open condition is interpretted as a logic one as the internal pullup resistors on PORTB are enabled by setting bit 7 of the OPTION register to zero.

	BCF OPTION_REG, NOT_RBPU	; weak pullups on PORTB

However, external 10K pullups are required on the SDA and SCL leads as the internal pullups are too large to provide the required current to the slave devices.

; Program 8591_2.ASM
;
; Illustrates how to use PCF8591 Digital to Analog capability as for
; waveform synthesis
;
; Program continually loops, each time checking inputs PORTB.1 and PORB.0
; which determine the waveform to be synthesized.
;
;	RB1	RB0
;	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 PORTB.3 and 
; PORTB.2
; 
;    PIC16C84				PCF8591
;
; RB7 (term 13) ------------------- SCL (term 10) ----- To Other
; RB6 (term 12) ------------------- SDA (term 9) ----- I2C Devices
;
;	------ RB3 (term 9)	speed select
;	------ RB2 (term 8)
;	
; 	------ RB1 (term 7)	waveform select
;	------ RB0 (term 6)
;
; 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, Nicole L. Ambrose, MSU, 11 July, '97

	LIST p=16c84
#include <c:\mplab\p16c84.inc>   
        __CONFIG 11h

	CONSTANT SDA=6
	CONSTANT SCL=7

	CONSTANT VARS=0CH

INDEX	EQU VARS+0
D_A_VAL EQU VARS+1
TEMP	EQU VARS+2
DEV_ADR EQU VARS+3	; A2, A1, A0 

_N	EQU VARS+4	; used for I2C routines
O_BYTE	EQU VARS+5
I_BYTE	EQU VARS+6

LOOP1	EQU VARS+7	; timing
LOOP2	EQU VARS+8	; timing

	ORG 000H	

	BSF STATUS, RP0	; RP1=0, RPO=1, BANK1
	MOVLW 0FH	; high nibble is outputs, lo nibble is inputs
        MOVWF TRISB	
	BCF OPTION_REG, NOT_RBPU	; weak pullups on PORTB
        BCF STATUS, RP0	; back to Bank 0

	MOVLW .1	
	MOVWF DEV_ADR	; A2 A1 A0 address

	CLRF INDEX	
	
BEGIN:
	MOVF PORTB, W	; determine which waveform
	ANDLW 03H	; read two least significant bits on PORTB
	MOVWF TEMP
	BCF STATUS, C	
	RLF TEMP, F	; multiply by 16
	RLF TEMP, F
	RLF TEMP, F
	RLF TEMP, F
	MOVF TEMP, W	; W = 16 * lowest two bits on PORTB
	ADDWF INDEX, W	; plus INDEX
	CALL WAVE_LOOKUP
	MOVWF D_A_VAL
	CALL D_A	; output value to D/A

	MOVF PORTB, W
	ANDLW 0CH	; bits 3 and 2 only
	MOVWF TEMP
	BCF STATUS, C	
	RRF TEMP, F
	RRF TEMP, F
	MOVWF TEMP	; W now contains PortB bits 3 and 2  
	CALL DELAY_LOOKUP
	MOVWF LOOP1	; LOOP1 now contains delay value from the lookup
			; table
	CALL DELAY_N_MS

	INCF INDEX, F
	MOVLW .16
	SUBWF INDEX, W
	BTFSS STATUS, Z
	GOTO BEGIN
	CLRF INDEX
	GOTO BEGIN
	
WAVE_LOOKUP:
	ADDWF PCL, F

	DT 00H, 10H, 20H, 30H, 40H, 50H, 60H, 70H	; sawtooth rising
	DT 80H, 90H, 0A0H, 0B0H, 0C0H, 0D0H, 0E0H, 0F0H

	DT 0F0H, 0E0H, 0D0H, 0C0H, 0B0H, 0A0H, 90H, 80H	; sawtooth falling
	DT 70H, 60H, 50H, 40H, 30H, 20H, 10H, 00H

	DT 00H, 20H, 40H, 60H, 80H, 0A0H, 0C0H, 0E0H	; triangular
	DT 0E0H, 0C0H, 0A0H, 80H, 60H, 40H, 20H, 00H

	DT 00H, 00H, 00H, 00H, 00H, 00H, 00H, 00H	; square wave
	DT 0FFH, 0FFH, 0FFH, 0FFH, 0FFH, 0FFH, 0FFH, 0FFH	

DELAY_LOOKUP:
	ADDWF PCL, F
	DT 1H, 2H, 4H, 8H	; number of millisecs

D_A:			; output D_A_VAL to PCF8591
	CALL START

	RRF DEV_ADR, W
	IORLW 90H
        MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK

        MOVLW 40H	; turn D/A output on
      	MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK  

        MOVF D_A_VAL, W	; output the D/A value
        MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK  

        CALL STOP
        RETURN

; The following routines are low level I2C routines applicable to most
; interfaces with I2C devices.

IN_BYTE				; read byte on i2c bus
	CLRF I_BYTE
	MOVLW .8
	MOVWF _N		; set index to 8	
	CALL HIGH_SDA		; be sure SDA is configured as input
IN_BIT
	CALL HIGH_SCL		; clock high
	BTFSS PORTB, SDA	; test SDA bit
	GOTO IN_ZERO
	GOTO IN_ONE

IN_ZERO
	BCF STATUS, C		; clear any carry
	RLF I_BYTE, F		; i_byte = i_byte << 1 | 0
	GOTO CONT_IN

IN_ONE
	BCF STATUS, C		; clear any carry
	RLF I_BYTE, F		
	INCF I_BYTE, F		; i_byte = (i_byte << 1) | 1
	GOTO CONT_IN

CONT_IN
	CALL LOW_SCL		; bring clock low
	DECFSZ _N, F		; decrement index
	GOTO IN_BIT
	RETURN

;;;;;;

OUT_BYTE:			; send o_byte on I2C bus
	MOVLW .8
	MOVWF _N
OUT_BIT:
	BCF STATUS,C		; clear carry
	RLF O_BYTE, F		; left shift, most sig bit is now in carry
	BTFSS STATUS, C		; if one, send a one
	GOTO OUT_ZERO
	GOTO OUT_ONE

OUT_ZERO:
	CALL LOW_SDA		; SDA at zero
	CALL CLOCK_PULSE	
	CALL HIGH_SDA
	GOTO OUT_CONT

OUT_ONE:
	CALL HIGH_SDA		; SDA at logic one
	CALL CLOCK_PULSE
	GOTO OUT_CONT

OUT_CONT:
	DECFSZ _N, F		; decrement index
	GOTO OUT_BIT
	RETURN	

;;;;;;
		
NACK:				; bring SDA high and clock
	CALL HIGH_SDA
	CALL CLOCK_PULSE
	RETURN

ACK:
	CALL LOW_SDA
	CALL CLOCK_PULSE
	RETURN

START:				
	CALL LOW_SCL
	CALL HIGH_SDA
	CALL HIGH_SCL
	CALL LOW_SDA		; bring SDA low while SCL is high
	CALL LOW_SCL
	RETURN

STOP:
	CALL LOW_SCL
	CALL LOW_SDA
	CALL HIGH_SCL
	CALL HIGH_SDA		; bring SDA high while SCL is high
	CALL LOW_SCL
	RETURN

CLOCK_PULSE:			; SCL momentarily to logic one
	CALL HIGH_SCL
	CALL LOW_SCL
	RETURN		

HIGH_SDA:			; high impedance by making SDA an input
	BSF STATUS, RP0	; bank 1
	BSF TRISB, SDA	; make SDA pin an input
	BCF STATUS, RP0	; back to bank 0
	CALL DELAY_SHORT
	RETURN

LOW_SDA:
	BCF PORTB, SDA	
	BSF STATUS, RP0	; bank 1
	BCF TRISB, SDA	; make SDA pin an output
	BCF STATUS, RP0	; back to bank 0
	CALL DELAY_SHORT
	RETURN

HIGH_SCL:
	BSF STATUS, RP0	; bank 1
	BSF TRISB, SCL	; make SCL pin an input
	BCF STATUS, RP0	; back to bank 0
	CALL DELAY_SHORT
	RETURN

LOW_SCL:
	BCF PORTB, SCL
	BSF STATUS, RP0	; bank 1
	BCF TRISB, SCL	; make SCL pin an output
	BCF STATUS, RP0	; back to bank 0
	CALL DELAY_SHORT
	RETURN

DELAY_SHORT:		; provides nominal 25 usec delay
	MOVLW .5
	MOVWF LOOP2
DELAY_SHORT_1:
	NOP
	DECFSZ LOOP2, F
	GOTO DELAY_SHORT_1
	RETURN 	

DELAY_LONG
	MOVLW .250	; 250 msec delay
	MOVWF LOOP1
DELAY_N_MS:
OUTTER
	MOVLW	.110	; close to 1.0 msec delay when set to .110
	MOVWF 	LOOP2
INNER
	NOP
	NOP
	NOP
	NOP
	NOP
	NOP
	DECFSZ	LOOP2, F	; decrement and leave result in LOOP2 
				; skip next statement if zero
	GOTO INNER
	DECFSZ 	LOOP1, F
	GOTO OUTTER
	RETURN

	END