paulv
Posts: 558
Joined: Tue Jan 15, 2013 12:10 pm
Location: Netherlands

Add an analog output to the Pi (DAC)

Sun Oct 25, 2015 3:29 pm

In a previous post, I showed how you can use the Pulse Width Modulation feature of the Pi to create a rather simple but precise (static) analog output level. Here is that post:
viewtopic.php?f=37&t=124130

In this post we’ll go one step further and create some dynamic wave forms and a very precise voltage level by using a DAC chip.
I used an ADC chip in another post to create a digital representation of an analog signal, the reverse of what we will be doing here. You can have a look at that post here :
viewtopic.php?f=37&t=123962
This post also deals with the installation of the SPI drivers for the kernel that we need for this application as well.

For the DAC, I selected an MPC4911. It has 10 bits resolution and an SPI interface, is commonly available and inexpensive. For more details, look at the datasheet:
http://ww1.microchip.com/downloads/en/D ... 22248a.pdf

The conversion process for the DAC uses a reference voltage. In this model, the reference is not included on the chip, so we need to supply one. For the most simple applications, we can use the 3V3 that is available on the P1 interface. The reference voltage also limits the output level, so we can only go up to 3V3 for the output level. We could have also used the 3V3 as a supply voltage, because the chip is happy with 2V7 already. In that case, just connect pin 1 and pin 6 together and connect it to the 3V3 supply. The diagram below shows the connections for this simple circuit with a separate reference voltage.
DAC-1.png
DAC-1.png (27.77 KiB) Viewed 16788 times
As with the MCP 3002, we’ll use the following pins to communicate with the device through the SPI bus. The SCLK for the clock, MOSI to send the data and one of the CE pins to select the device. We don’t get anything back from this chip, so MISO is not used.
The LDAC input can be used to select the device, and you could use a GPIO pin to turn the device on and off through this pin. In this example, we just tie it to ground so it is always selected and powered.

Let us now switch to the required software.

The conversion process we need works as follows. A digital number represents the analog output level in 10 bit steps, or in 1024 different values. With a reference voltage of 3V3, you can set the output in 3300mV / 1024 = 3.22mV steps, which is also the precision of the output, analog to the MPC 3002.

A very simple program can be constructed as follows:

Code: Select all

#!/usr/bin/python
#-------------------------------------------------------------------------------
# Name:        MCP4911.py
# Purpose:     Set a static output level for the DAC
#
# Author:      paulv
#
# Created:     18-09-2015
# Copyright:   (c) paulv 2015
# Licence:     <your licence>
#-------------------------------------------------------------------------------

import spidev
from time import sleep
#import RPi.GPIO as GPIO

DEBUG = True
spi_max_speed = 4 * 1000000 # 4 MHz
V_Ref = 3300 # 3V3 in mV
Resolution = 2**10 # 10 bits for the MCP 4911
CE = 0 # CE0 or CE1, select SPI device on bus

# setup and open an SPI channel
spi = spidev.SpiDev()
spi.open(0,CE)
spi.max_speed_hz = spi_max_speed

# transfer of the data to the output of the DAC can be done at the rising edge
# of the !CS pin, in which case the !LDAC pin can be tied to GND.
# Otherwise, use a GPIO pin to drive the !LDAC pin low.
# Here we could drive the !LDAQ pin low as soon as this program runs when you
# remove the comments for all the GPIO related lines.
#LDAQ = 22 # Can use any pin, here we use GPIO 22
#GPIO.setmode(GPIO.BOARD)
#GPIO.setup(LDAQ, GPIO.OUT)
#GPIO.output(LDAQ,GPIO.LOW)

def setOutput(val):
    # lowbyte has 6 data bits
    # B7, B6, B5, B4, B3, B2, B1, B0
    # D5, D4, D3, D2, D1, D0,  X,  X
    lowByte = val << 2 & 0b11111100
    # highbyte has control and 4 data bits
    # control bits are:
    # B7, B6,   B5, B4,     B3,  B2,  B1,  B0
    # W  ,BUF, !GA, !SHDN,  D9,  D8,  D7,  D6
    # B7=0:write to DAC, B6=0:unbuffered, B5=1:Gain=1X, B4=1:Output is active
    highByte = ((val >> 6) & 0xff) | 0b0 << 7 | 0b0 << 6 | 0b1 << 5 | 0b1 << 4
    # by using spi.xfer2(), the CS is released after each block, transferring the
    # value to the output pin.
    if DEBUG :
        print("Highbyte = {0:8b}".format(highByte))
        print("Lowbyte =  {0:8b}".format(lowByte))
    spi.xfer2([highByte, lowByte])

try:
    while(True):
        outputlevel = input('Enter an output level from 0-1023 : ')
        if DEBUG : print "Binary value is : {0:10b} (10 bit)".format(outputlevel)
        # with a DAC gain of 1x
        print "Output level should be : {0} mV".format(outputlevel * V_Ref / Resolution)
        setOutput(outputlevel)
        sleep(1)

except (KeyboardInterrupt, Exception) as e:
    print(e)
    print "Closing SPI channel"
#    GPIO(cleanup)
    spi.close()

def main():
    pass

if __name__ == '__main__':
    main()
With this, you can program a voltage with a 3.22mV resolution between 0 and 3V3. If you need to have a higher voltage, look at the post for the ADC, it details how you can use an Op Amp solution to get higher voltages.

If you want to get one step further with the DAC, the previous program can easily be changed to generate other wave forms. Here is one example for a saw tooth (a ramp).

Code: Select all

#!/usr/bin/python
#-------------------------------------------------------------------------------
# Name:        MCP4911_sawtooth.py
# Purpose:     Output a sawtooth waveform
#
# Author:      paulv
#
# Created:     18-09-2015
# Copyright:   (c) paulv 2015
# Licence:     <your licence>
#-------------------------------------------------------------------------------

import spidev
from time import sleep

DEBUG = True
spi_max_speed = 4 * 1000000 # 4 MHz
V_Ref = 3300 # 3V3 in mV
Resolution = 2**10 # 10 bits for the MCP 4911
CE = 0 # CE0 or CE1, select SPI device on bus

# setup and open an SPI channel
spi = spidev.SpiDev()
spi.open(0,CE)
spi.max_speed_hz = spi_max_speed


def setOutput(val):
    # lowbyte has 6 data bits
    # B7, B6, B5, B4, B3, B2, B1, B0
    # D5, D4, D3, D2, D1, D0,  X,  X
    lowByte = val << 2 & 0b11111100
    # highbyte has control and 4 data bits
    # control bits are:
    # B7, B6,   B5, B4,     B3,  B2,  B1,  B0
    # W  ,BUF, !GA, !SHDN,  D9,  D8,  D7,  D6
    # B7=0:write to DAC, B6=0:unbuffered, B5=1:Gain=1X, B4=1:Output is active
    highByte = ((val >> 6) & 0xff) | 0b0 << 7 | 0b0 << 6 | 0b1 << 5 | 0b1 << 4
    #
    # by using spi.xfer2(), the CS is released after each block, transferring the
    # value to the output pin.
    if DEBUG :
        print("Highbyte = {0:8b}".format(highByte))
        print("Lowbyte =  {0:8b}".format(lowByte))
    spi.xfer2([highByte, lowByte])


try:
    while(True):
        # create a sawtooth ramp starting from 0 to V-ref in 1024 steps
        for step in range(1025):
            if DEBUG :
                print "Step = {0}". format(step)
                print "Output level should be : {0} mV".format(step * V_Ref / Resolution)

       	    setOutput(step)

            if DEBUG :
                sleep(0.2)
            else :
                # if you use a DMM to track the output, leave the sleep in.
                # if you use a scope, set the sleep at 0.0
                sleep(0.01)


except (KeyboardInterrupt, Exception) as e:
    print(e)
    print "Closing SPI channel"
    spi.close()

def main():
    pass

if __name__ == '__main__':
    main()
This again can easily be changed to create a triangle waveform, by adding another for-next loop counting down to 0.

Finally, here is a program that outputs a sine wave.

Code: Select all

#!/usr/bin/python
#-------------------------------------------------------------------------------
# Name:        MCP4911_sinewave.py
# Purpose:     Generate a sinewave
#
# Author:      paulv
#
# Created:     18-09-2015
# Copyright:   (c) paulv 2015
# Licence:     <your licence>
#-------------------------------------------------------------------------------


import spidev
from time import sleep
import math

DEBUG = False
spi_max_speed = 4 * 1000000 # 4 MHz
V_Ref = 3300 # 3V3 in mV
Resolution = 2**10 # 10 bits for the MCP 4911
CE = 0 # CE0 or CE1, select SPI device on bus

# setup and open an SPI channel
spi = spidev.SpiDev()
spi.open(0,CE)
spi.max_speed_hz = spi_max_speed


def setOutput(val):
    # lowbyte has 6 data bits
    # B7, B6, B5, B4, B3, B2, B1, B0
    # D5, D4, D3, D2, D1, D0,  X,  X
    lowByte = val << 2 & 0b11111100
    # highbyte has control and 4 data bits
    # control bits are:
    # B7, B6,   B5, B4,     B3,  B2,  B1,  B0
    # W  ,BUF, !GA, !SHDN,  D9,  D8,  D7,  D6
    # B7=0:write to DAC, B6=0:unbuffered, B5=1:Gain=1X, B4=1:Output is active
    highByte = ((val >> 6) & 0xff) | 0b0 << 7 | 0b0 << 6 | 0b1 << 5 | 0b1 << 4
    #
    # by using spi.xfer2(), the CS is released after each block, transferring the
    # value to the output pin.
    if DEBUG :
        print("Highbyte = {0:8b}".format(highByte))
        print("Lowbyte =  {0:8b}".format(lowByte))
    spi.xfer2([highByte, lowByte])

try:
    while(True):
        for angle in range(1, 360):
            # generate a sine wave
            val = math.sin(math.radians(angle))
            # results in values between -1 and +1
            # add 1 to make all values positive (so between 0 and 2)
            # scale it to get only positive numbers between 0 and 256
            # Use only integer numbers
            val = int((val + 1 ) * 1024 / 8)

            if DEBUG : print "Output value is {0:10b}".format(val)

       	    setOutput(val)

            if DEBUG :
                sleep(0.2)
            else :
                # if you use a DMM to track the output, leave the sleep in.
                # if you use a scope, set the sleep to 0.0
                sleep(0.05)


except KeyboardInterrupt:
    print "Closing SPI channel"
    spi.close()

def main():
    pass

if __name__ == '__main__':
    main()
With this under your belt, you can now create your own function generator, albeit within limits of the possible frequency. The frequency is determined by the SPI clock speed, and works differently then you probably guessed. Search this forum for more details about the spi.max_speed_hz settings for the SPI interface.

You will have noticed that the resolution per step is a bit awkward, because of the division by 1024, the resolution of the 10-bit chip. Luckily, there are voltage reference chips available that output a voltage reference level of 1024 mV or multiples of that value. This allows you to create more sensible steps, and with the gain function of the DAC set to 2x, you have now more flexibility if your application needs it.

Have fun!

paulv

gerin99
Posts: 10
Joined: Mon Jan 11, 2016 9:55 pm

Re: Add an analog output to the Pi (DAC)

Mon Jan 11, 2016 9:57 pm

What in your first example code would need to change if I were to use the MCP4921 (12-bit chip)? I have been playing around and I can get the 12-bit chip to work with the 10-bit functions/code, but I cannot figure out what to change to get the code to work in 12-bit.

Thank you

paulv
Posts: 558
Joined: Tue Jan 15, 2013 12:10 pm
Location: Netherlands

Re: Add an analog output to the Pi (DAC)

Tue Jan 12, 2016 5:38 am

gerin99,

You need to look at page 18 of the MCP4921 datasheet where the bits of the write command register are detailed. The difference between the two chips is only in the lower byte of the register. Instead of 6 data bits for the MCP4911, the MCP4921 has all 8 bits occupied.

Basically, you can go through the code in the first example and change all references from 10 bit to 12 bits.

Code: Select all

Resolution = 2**10 # 10 bits for the MCP 4911
becomes

Code: Select all

Resolution = 2**12 # 12 bits for the MCP 4921
You need to change the lowbyte from 6 bits to 8 bits in setOutput

Code: Select all

def setOutput(val):
    # lowbyte has 6 data bits
    # B7, B6, B5, B4, B3, B2, B1, B0
    # D5, D4, D3, D2, D1, D0,  X,  X
    lowByte = val << 2 & 0b11111100
Basically, you need to omit the shift left and change the "OR" value so this piece becomes:

Code: Select all

def setOutput(val):
    # lowbyte has 8 data bits
    # B7, B6, B5, B4, B3, B2, B1, B0
    # D7, D6, D5, D4, D3, D2, D1, D0
    lowByte = val & 0b11111111
The comments in setOutput change for the highbyte, but there are no code changes:

Code: Select all

    # highbyte has control and 4 data bits
    # control bits are:
    # B7, B6,   B5, B4,     B3,  B2,  B1,  B0
    # W  ,BUF, !GA, !SHDN,  D9,  D8,  D7,  D6
Becomes:

Code: Select all

    # highbyte has control and 4 data bits
    # control bits are:
    # B7, B6,   B5, B4,     B3,  B2,  B1,  B0
    # W  ,BUF, !GA, !SHDN,  D11, D10, D9,  D8
Finally, you also need to change one line in the main() portion

Code: Select all

if DEBUG : print "Binary value is : {0:10b} (10 bit)".format(outputlevel)
Change it to

Code: Select all

if DEBUG : print "Binary value is : {0:12b} (12 bit)".format(outputlevel)
.
I don't have that chip so I can't test it, but this should do.

Good luck!

luvlavs
Posts: 1
Joined: Wed Jun 08, 2016 4:56 pm

Re: Add an analog output to the Pi (DAC)

Wed Jun 08, 2016 5:19 pm

Hi,

I would like to use AD5683R 16 bit DAC board. Reading through the datasheet, the input register is 24 bit wide ( 4 bits are command bits and 20 bits are data bits). How can I change your code to work for this board?

You help is really appreciated. Thank you

paulv
Posts: 558
Joined: Tue Jan 15, 2013 12:10 pm
Location: Netherlands

Re: Add an analog output to the Pi (DAC)

Wed Jun 08, 2016 8:06 pm

I really can't help you, but if you compare the 5683 data sheet with that of the 4911 and the 4921 and then follow the comments in the example code, you should be able to figure this out. It's really not that difficult, and assuming you have the chip, you can try things out until it works.

Alternatively, you can create a new post with the AD5683R in the subject text and maybe somebody that already uses that chip can chime in.

Success!

davidwill25
Posts: 1
Joined: Tue Jul 03, 2012 1:03 pm

Re: Add an analog output to the Pi (DAC)

Thu Oct 20, 2016 1:10 pm

Hello,

I have followed this example as adding an analog to the pi is required for a project I am working on at the moment.
I am having an issue with the following line:

Code: Select all

spi.open(0, CE)
Where CE is set to 0.
The error is IOError: [Errno 2] No such file or directory
I realise that this is to do with missing SPI files and I have turned on SPI using raspi-config and rebooted.
I still get the same error.
This is actually the second time I have had the error (with the same project, I then bricked my OS trying to correct it) so this was a clean install of Jessie. With the instructions followed to install the spidev from the linked post in this one.

Anyone have any ideas how to sort the issue out?
Thanks,
David

EDIT: I got it working by doing a complete clean install of Jessie and swapping to an RPi 2. Don't know if I cocked something up by trying to follow the instructions and install the SPIDEV or what.

ElEscalador
Posts: 671
Joined: Tue Dec 15, 2015 4:55 pm
Location: Detroit, MI USA
Contact: Website

Elescalador

Thu Oct 20, 2016 3:31 pm

Awesome. Thank you.
My Autonomous Robot Project and a few of my other projects below.

https://lloydbrombach.wordpress.com/

Magnus354
Posts: 3
Joined: Mon Feb 06, 2017 8:46 am

Re: Add an analog output to the Pi (DAC)

Mon Feb 06, 2017 8:50 am

Hey,

thank you for this post, it's very interesting for me!
So, I've got a question, how do you record the analog signal after doing all this stuff? How can I test that it works correctly?

best regards
Magnus

paulv
Posts: 558
Joined: Tue Jan 15, 2013 12:10 pm
Location: Netherlands

Re: Add an analog output to the Pi (DAC)

Mon Feb 06, 2017 10:50 am

Hi Magnus354,

I'm not sure that I understand what you mean by your question to "record" the resulting signal.

To see the result of the digital to analog conversion, you can use an oscilloscope or an analog data logger to look at the signal, and record it.
You can also use an analog to digital converter device to convert the analog signal back to digital and record that. ( have a forum post for AD convertors too)

If you mean something else, please see if you can be more specific.

Enjoy!

kosaku
Posts: 1
Joined: Fri Mar 17, 2017 3:22 pm
Location: JAPAN

Re: Add an analog output to the Pi (DAC)

Fri Mar 17, 2017 4:04 pm

Hi, paulv

Thank you so much for your code.
but, it didn't work very well for MCP4921(actually use MCP4922).

I think that it is better to change the following code as well.

Code: Select all

highByte = ((val >> 6) & 0xff) | 0b0 << 7 | 0b0 << 6 | 0b1 << 5 | 0b1 << 4
becomes

Code: Select all

highByte = ((val >> 8) & 0xff) | 0b0 << 7 | 0b0 << 6 | 0b1 << 5 | 0b1 << 4
If it is "6", the resolution shrinks to 10 bits.
So I tried 8 bit shift and I could output with 12 bit resolution.
Because highbyte must also shift right by 2 bits like lowbyte.

Thank you.
Last edited by kosaku on Sat Mar 18, 2017 3:41 am, edited 1 time in total.

paulv
Posts: 558
Joined: Tue Jan 15, 2013 12:10 pm
Location: Netherlands

Re: Add an analog output to the Pi (DAC)

Mon Mar 20, 2017 8:09 am

Good point kosaku, I overlooked that additional change and it proves once again that you need to test your code...
Thanks for contributing!

alexanderstibor
Posts: 1
Joined: Fri May 26, 2017 1:39 am

Re: Add an analog output to the Pi (DAC)

Fri May 26, 2017 1:47 am

Hi!
Above there was a problem I also face. Unfortunately, nobody gave a solution. When starting the program MCP4911.py I get the error message:

Traceback (most rescent call last):
File "MCP4911.py", line 25, in <module>
spi.open(0,CE)
IOError: [Errno2] No such file or directory

I use a Raspberry 3. Does anybody know how to deal with this problem?

Thank you,
Alex

paulv
Posts: 558
Joined: Tue Jan 15, 2013 12:10 pm
Location: Netherlands

Re: Add an analog output to the Pi (DAC)

Fri May 26, 2017 9:56 am

Hi Alexander,

Can you please verify that you have gone through the following steps without errors reported?:

Code: Select all

sudo apt-get install python-setuptools -y
sudo apt-get install python-dev -y
sudo apt-get install git -y
# if installed previously : sudo rm -R py-spidev
git clone git://github.com/doceme/py-spidev
cd py-spidev/
sudo python setup.py install
# Edit /boot/config.txt and add:
dtparam=spi=on
# reboot to activate
If possible, do that on a new and clean installation of Jessie (lite).

At this moment, I am working on a project with the MCP4921 (12-bit version of the 10-bit 4911), and I am using the latest and updated Jessie Lite version, and everything works fine.

paulv
Posts: 558
Joined: Tue Jan 15, 2013 12:10 pm
Location: Netherlands

Re: Add an analog output to the Pi (DAC)

Fri May 26, 2017 2:44 pm

In the previous post I mentioned that I have a project with the MCP4921, the 12-bit version DAC.
I put together a small script that allows you to input the digital bit code through the console.

Here is that Python script:

Code: Select all

#!/usr/bin/python
#-------------------------------------------------------------------------------
# Name:        MCP4921 12 bit DAC with console input
# Purpose:
#
# Author:      paulv
#
# Created:     18-09-2015
# Copyright:   (c) paulv 2015
# Licence:     <your licence>
#-------------------------------------------------------------------------------

# sudo apt-get install python-setuptools -y
# sudo apt-get install python-dev -y
# sudo apt-get install git -y
# if installed previously : sudo rm -R py-spidev
# git clone git://github.com/doceme/py-spidev
# cd py-spidev/
# sudo python setup.py install
# Edit /boot/config.txt and add:
# dtparam=spi=on
# reboot to activate


import spidev
from time import sleep
#import RPi.GPIO as GPIO
DEBUG = False


# setup and open an SPI channel
#spi_max_speed = 4 * 1000000 # 4 MHz
spi = spidev.SpiDev()
spi.open(0,0)
#spi.max_speed_hz = spi_max_speed

def setOutput(val):
    # lowbyte has 8 data bits D0..D7
	lowByte = val & 0xff; # 0b 1111 1111
    # highbyte has control and data bits
    # control bits are:
    # B7, B6,   B5, B4,     B3,  B2,  B1,  B0
    # W  ,BUF, !GA, !SHDN,  B11,  B10,  B9,  B8
    # B7=0:write to DAC, B6=0:unbuffered, B5=1:Gain=1X, B4=1:Output is active
	highByte = ((val >> 8) & 0xff) | 0b0 << 7 | 0b0 << 6 | 0b1 << 5 | 0b1 << 4;
    # by using spi.xfer2(), the CS is released after each block, transferring the
    # value to the output pin.
	spi.xfer2([highByte, lowByte])


try:
    print "input data to an MCP4921 12-bit DAC"
    print "Data can be from 0..4095"

    while(True):
        try:
            newbits = int(raw_input('Input: '))
        except ValueError:
            print "Not a number"
            continue
        print ('Number %s \n' % (newbits))

        setOutput(newbits)


except KeyboardInterrupt:
    print "\nClosing SPI channel"
    spi.close()

def main():
    pass

if __name__ == '__main__':
    main()
Enjoy!

Paulcm
Posts: 1
Joined: Fri Apr 06, 2018 7:22 am

Re: Add an analog output to the Pi (DAC)

Fri Apr 06, 2018 7:24 am

Hi Paul,

Thanks for your codes, this helped me a lot to understand what's going on !

I would like to control a piezo actuator with 2 input channels with a RasPi. For this, I will need a DAC and an amplifier. I still don't know if I will use two 1-channel DACs or 1 DAC with 2 channels.
In any case: is it possible to control 2 channels simultaneously ? (by using CE0 and CE1 at the same time) Or can CE0 and CE1 be only used separately? The information might not be the same for the 2 channels !
I heard about LDAC etc but not sure how to use it...
Do you have any idea?

Thank you,
Paul

paulv
Posts: 558
Joined: Tue Jan 15, 2013 12:10 pm
Location: Netherlands

Re: Add an analog output to the Pi (DAC)

Sat Apr 07, 2018 6:20 am

Hi Paul,

Using CE0 and CE1 is exactly how you describe. You can use these two hardware "selectors" to control two separate devices. Alternatively, you can use one chip with two (or more) devices and select them in software (controlling a "selector "bit within the control byte/word of the chip) in a similar way. In both cases, you control both DAC's and their inputs completely separate in your code.

These chips are inexpensive, get a couple and give it a try, it's not that hard actually.

Enjoy!

Return to “Automation, sensing and robotics”