Using PWM as a stabilised DAC (even with an unregulated power supply)


Senior Member

Most PICaxes have an internal Digital to Analogue Converter, but it has several major limitations. Firstly it has only 5-bit resolution (32 voltage levels) and a high output impedance (up to 40k), so it often requires an external buffer/amplifier. Also, with most M2 devices, the DAC shares the same pin with the Serial/Programming Output, which is inconvenient (but still usable).

So it is often better to use one of the PWM outputs as a DAC. The output impedance is low (but adding any required Low-Pass filter will increase this) and it offers 10-bit resolution (1024 levels). However, if the PICaxe has an unregulated power supply then the DAC output normally "tracks" these changes. If a stabilised output voltage is required (e.g. to drive a meter) then the internal DAC can be fed from the Fixed Voltage Reference (FVR), but this is not possible with a PWM output. However, as discussed in post #12 of this thread the supply rail (Vdd) variations can be compensated by multiplying by 1/Vdd. Normally, the reciprocal would be an issue (because PICaxe Basic division gives only single-byte resolution), but it happens that CALIBADC10 actually delivers 1/Vdd, so no division process is required.

Unfortunately, CALIBADC10 gives only around 8-bit resolution in practice, so the accuracy in compensating a 10-bit PWM output would be severely compromised. But this code snippet shows how higher resolution versions of CALIBADC can be devised. In that thread the nominal resolution is increased by powers of two, but here I have used code which gives a factor of 20, which is somewhat faster (for the accuracy). In this code snippet, I have aimed for a reasonably high resolution (around 12 bits) which, perhaps surprisingly, simplifies the calculation slightly, by using the ** operator alone (** multiplies two variables but then effectivley divides the result by 65536, so it is mainly useful where the variables are very large compared with byte values).

To calculate the stabilised PWM value, we need to multiply the desired (byte) value by a Calibration Constant and by 1/Vdd to produce a 10-bit word for the PWM hardware. The multiplications can be performeed in any sequence, but here the byte value is multiplied by 16 before adding the "bias" calibration constant (i.e. the bias adjusts the output level in sixteenths of a Least Significant Bit). The bias compensates for any "zero offset" error (e.g. of a meter) and also introduces "rounding" correction for the calculation (i.e. a value of 8 puts the calulated result near the centre of the required pulse-range). This first stage of the calculation produces a result up to around 5000, so this is then scaled up by a further factor of 8, before using the ** operator to multiply by a "gradient" calibration constant of nominally 8192, to produce a result up to about 6000.

The CALIBADC value returned by the subroutine is around 5000 at a Vdd of 4 volts (but could be in excess of 8191), so it is multiplied by 4 before using the ** operator again to multiply by the previously calculated value. This result is (intentionally) twice the size required by the PWM hardware, so it is rounded up (when appropriate) by adding 1, before dividing by 2. The resulting value would normally be sent to the PWM hardware with a PWMOUT command, but here PWMDUTY is used, to work around a bug in the PICaxe 20M2 interpreter code.

; DAC Output using Vdd-compensated PWM
; AllyCat, October 2015 

#picaxe 08M2			; Or any M2 (with appropriate numbers and bug workaround in 20M2)
#terminal 4800

symbol BIAS 	= 8 	  				; Calibrate Zero Output (BIAS) Level (nominally 1/2 LSB = 8)
symbol CALPWM 	= 8150; 				; Calibrate PWM output. If meter divider: multiply by (Rs+Rp)/Rp
symbol char	= b0					; Byte to output via PWM 
symbol tempb 	= b1					; Temporary or "Local" byte variable
symbol tempw	= w1 					; Temporary or "Local" word variable
symbol acc	= w3					; Accumulates calibadcx total 
symbol PWMOP 	= b.2					; PWM output pin (08M2 = b/c.2 ; 14M2 = b.2 ; 20M2 = b.1)

;   pwmout PWMDIV4,PWMOP,255,0	; Only required for 20M2,other M2s can use PWMOUT in main loop
	for char = 0 to 255
		call calibadcx
		call calcpwm
		sertxd(#char," ",#tempw,cr,lf)
		pause 1000
calibadcx:			;* Extended resolution CALIBADC routine (= CALIBADC10 * 20.125)
 	adcconfig 0			; ADC ref to Vdd	(not done within calibadc)
	calibadc10 acc			; Clear/initialise accumulator
	fvrsetup fvr2048		; Nominal Fixed Voltage Reference = 2048 mV
	dacsetup $88			; Reference chain to FVR
   for tempb = 20 to 31			; Loop for multiple measurements (6*51=306)/32
      daclevel tempb			; Change attenuation factor
	   readdac10 tempw		; Read voltage on "wiper"
      acc = acc + tempw			; Accumulate extended CALIBADC value. (306+16=322)/16
  	next				; Typical result 20*CALIBADC10 = 5120 (@4v)
	return				; "CALIBADC" value in acc (max 12000, typically 4000 @5v)
calcpwm:		; Enter with byte in char, add bias, multiply by CALPWM and by 1/Vdd(=acc)
   tempw = char * 16 + BIAS * 8		; Char * 16 + Bias * 8 = Typical maximum 33000 
   tempw = tempw ** CALPWM		; (256*16*8*8k/64k=4k Typical maximum)   	      
   tempw = acc * 4 ** tempw + 1		; Full-scale = 2048 so round up and divide by 2
   tempw = tempw / 2 max 1023 		; (400*20)*4*4k/64k/2=1k Typical @Vdd=2.5v)
setpwm:					; Fall into here from calcpwm (or enter as a subroutine)
;	pwmduty PWMOP,tempw		; PWMDUTY avoids bug with 20M2s (other M2s can use PWMOUT)
	pwmout PWMDIV4,PWMOP,255,tempw	; More compact version for all M2s except 20M2   
Cheers, Alan.