Arduino UNO 16MHz , MCP4922 SPI drived: Objective: kHz 16bits Saw tooth

Hi,

I would have liked to use a DAC operating using the SPI protocol in order to generate a saw tooth having a 2 ms period.

Believing my dream was impossible to achieve with a serial DAC, I turned to the LTC4050CN using HC TTL logic and this version may turns lot of times faster if needed.

By chance, reading an authorized comment in the following section:

https://forum.arduino.cc/index.php?topic=483715.0

I met a serious hope of being able to use an SPI DAC since someone speaking of work in a completely realistic way with DAC MCP4922 at a frequency ranging 25 kHz.!

My problem was only restricted to 5 kHz so I have expected to have a chance...

I bought an MCP4922 to give it a try.

  1. To carry out this test, I have used the following Github library:
  1. When I measured the period of the generated sawtooth, I greatly simplified the given example because I want to work with an Arduino Uno (official item) equipped with a 16 MHz crystal, and I absolutely have to use the precision given by the 12 bits of the DAC.

  2. I have simplified the program to avoid unnecessary waste of time and I use only Channel A of the MCP4922.

  3. By measuring the period of the clock SCK, I noticed that the clock SPI worked at 4 MHz, I added a line in order to witch it to 8 MHz:
    SPI.setClockDivider (SPI_CLOCK_DIV2);

  4. While examining the CPP file, I replaced all “digitalWrite ()” instructions with faster accesses to the PORT B and PORTD used by CS and LDAC /.

  5. I have attach a compressed file containing the modified library and, as an example, the program that I am using to create the “fastest” Saw tooth described here…

As these signals are repetitive, the use of an analog oscilloscope is enough to picture timings scope but to be able to have correct scope printouts, I have used my old Tektronix TDS360 and you will find a copy in the Zipped file.

On these Scope printouts you will see:

  1. The saw tooth (Fig 1) whose period of 15 msec could be further improved by a few microseconds if I left the LDAC / signal permanently tied to zero volts.
    You will notice that the maximum frequency obtained is 1 /0.015 sec. (or 66 Hz) therefore very far from the announced 25 kHz and my question is quite simple:

What can I modify and how to modify it to reach 5 kHz target ?

  1. Fig 2 shows the timing of CS / versus LDAC /.
  2. Fig 5 being an enlarged view of LDAC / with respect to the rising edge of CS /
  3. Fig 3 shows the timing of CS / in relation to CSK.
  4. Fig 6 highlights the delay between the falling edge of CS / and the rising edge of the first clock in the series of 2 times 8 pulses.
  5. Fig 8 highlights the time between the 2 transfers of 8 bits in the MCP4922 registers.
  6. Fig 7 highlights the time separating the 16th SCK pulse from the rising edge of CS /
  7. As for fig 4 it shows an enlarged shape of clock SCK.

I am unfortunately not a great specialist in C or C ++ (nor in ATMega Assembler) but I suppose that this forum is not lacking of very skilled professionals and maybe one of them could really help me achieve the repetition frequency of 5 kHz?

That said, despite my great age, I am ready to make a serious effort to improve my C (++) and ATMega Assembler knowledge.

French is my mother language so I pray you to excuse my very poor English!

Thanks in advance for your help....:

Objective 5 kHz using Arduino Uno 16 MHz and MCP4922, one DAC used with 16 bits resolution.!

MCP4922F-Arduino-SPI-Library-master.zip (3.8 KB)

timing MCP4922 _FAST.pdf (64.1 KB)

read DAC with 12bits resolution...sorry

So your code comes down to this?

void loop(){
   while (1){
   PORTB = PORTB & 0xFE; // clear D8 for CS/, other bits stay where they were, or use fastDigitalWrite, or bitSet(), or ...
   SPI.transfer (upperByte); // contains 4 bits command info, 4 bits of data, commands do not change
   SPI.transfer (lowerByte); // contains 8 bits data info
   PORTB = PORTB | 0x01; // set D8 for CS, other bits stay where they were, use use fastDigitalWrite, or bitClear(), or ...
   outputDAC = outputDAC + value;
     if (outputDAC == 0xFFF) {
     value = -1;
     }
     if (outputDAC == 0){
     value = 1;
     }
   upperByte = commandNibble | ((outputDAC & 0x0F00) >>4) );
   lowerByte = outputDAC & 0xFF;
   } // end while
} // end loop

If you run that, how fast can the cycle be? If not fast enough, then 'value' may need to increase to make steps of 2,3,4 etc, to fit in the 2mS window.

Can also do some things like add this to the top of your sketch
#define nop asm volatile ("nop")

and do this instead of SPI.transfer()
SPDR = (dataToSend);
{nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop;}
to avoid waiting out the interrupts for the transfer

You can even turn interrupts off to prevent any glitchiness from the micros() and millis() timers.

With the serial print, I am seeing 36.8mS per cycle. Didn't try a scope to see what taking out the Serial.print is doing.
Counting by 4, I get 9.4mS.

#include <SPI.h>
#define nop asm volatile ("nop")
unsigned int outputDAC;
int value = 1;
byte commandNibble = 0xF0; // set bits as needed
byte upperByte;
byte lowerByte;
unsigned long startMicros;
unsigned long endMicros;
unsigned long elapsedMicros;


void setup() {
  SPI.begin();
  SPI.setClockDivider(SPI_CLOCK_DIV2); // 8 MHz
  Serial.begin(500000);
  pinMode (8, OUTPUT); // D8 as chip select
}


void loop() {
  while (1) {
    PORTB = PORTB & 0xFE; // clear D8 for CS/, other bits stay where they were, or use fastDigitalWrite, or bitSet(), or ...
    SPDR = (upperByte); nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; // wait out the xfer
    //SPI.transfer (upperByte); // contains 4 bits command info, 4 bits of data, command bits do not change
    SPDR = (lowerByte);  nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; // wait out the xfer
    //SPI.transfer (lowerByte); // contains 8 bits data info
    PORTB = PORTB | 0x01; // set D8 for CS, other bits stay where they were, use use fastDigitalWrite, or bitClear(), or ...
    outputDAC = outputDAC + value;
    if (outputDAC == 0xFFF) {
      value = -1; // count down
    }
    if (outputDAC == 0) {
      value = 1; // count up
      Serial.println (micros() );
    }
    upperByte = commandNibble | ((outputDAC & 0x0F00) >> 4) ;
    lowerByte = outputDAC & 0xFF;
  } // end while
} // end loop

40uS, you're going to have to take much bigger steps.

Counting by 0xff, you can get to 424uS.

#include <SPI.h>
#define nop asm volatile ("nop")
unsigned int outputDAC;
int value = 1;
byte commandNibble = 0xF0;
byte upperByte;
byte lowerByte;
unsigned long startMicros;
unsigned long endMicros;
unsigned long elapsedMicros;


void setup() {
  SPI.begin();
  SPI.setClockDivider(SPI_CLOCK_DIV2);
  Serial.begin(500000);
  pinMode (8, OUTPUT);
}


void loop() {
  while (1) {
    PORTB = PORTB & 0xFE; // clear D8 for CS/, other bits stay where they were, or use fastDigitalWrite, or bitSet(), or ...
    SPDR = (upperByte);nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; // wait out the xfer
    //SPI.transfer (upperByte); // contains 4 bits command info, 4 bits of data, commands do not change
    SPDR = (lowerByte);nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; // wait out the xfer
    //SPI.transfer (lowerByte); // contains 8 bits data info
    PORTB = PORTB | 0x01; // set D8 for CS, other bits stay where they were, use use fastDigitalWrite, or bitClear(), or ...
    outputDAC = outputDAC + value;
    if (outputDAC == 0x0ff0) {
      value = -0xff; // count down
    }
    if (outputDAC == 0) {
      value = 0xff; // count up
      Serial.println (micros() );
    }
    upperByte = commandNibble | ((outputDAC & 0x0F00) >> 4) ;
    lowerByte = outputDAC & 0xFF;
  } // end while
} // end loop

Still a long ways from 40.

Thread moved to the English section.

@Jean_mbk, if you want this thread moved to a French section (or any other section) just click Report to moderator then ask.

Thank you for your very fast, very interesting and constructive response.

I apologize don't have specified my wiring when I have posted my question.
Therefore I have adapted your program to my Arduino' pinouts and you will find above
a document showing the wiring and the Arduino Ports used.

I resumed the test with a a scope picture and you will see that I get a very fast saw tooth
(Period is +-1.2 millisecond but the waveform amplitude is less than .2V in front of the 5V expected.
I guess you must have a idea to improve this situation?
Have a nice day.

:confused: How may I proceed to send you 2 pictures?...

Thanks for your help, I just be little smarter.

The calculation of upperByte and lowerByte was wrong.
This problem is now fixed.
Here is the program as it is.
The saw tooth generated has a peak voltage of 5v, but its frequency is 55 Hz, so slower than that which I produce with my program....

Perhaps have you an good idea to deeply increase saw tooth frequency?

#include <SPI.h>
#define nop asm volatile ("nop")

unsigned int outputDAC;
int value = 1;
byte commandNibble = 0x70;
byte upperByte;
byte lowerByte;

void setup() {
SPI.begin();
SPI.setClockDivider(SPI_CLOCK_DIV2);

pinMode (5, OUTPUT);// pin LDAC/
pinMode (10, OUTPUT);//pin CS/
pinMode (11, OUTPUT);//pin MOSI or SDI
pinMode (13, OUTPUT);// SCK Clock
}

void loop() {
while (1) {
PORTB = PORTB & 0xFB;
SPDR = (upperByte);nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; // wait out the xfer
SPDR = (lowerByte);nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; nop; // wait out the xfer
PORTB = PORTB | 0x04;

outputDAC = outputDAC + value;
if (outputDAC == 0xFFF) { value = -1; }
if (outputDAC == 0) {value = 0xFFF; }

upperByte = commandNibble | ((outputDAC & 0x7F00)>>8 ) ;
lowerByte = outputDAC & 0x00FF;

} // end while
} // end loop

A lot of thanks for your very valuate help!

Make bigger steps -> value = 2, 4, 16, etc.

I suppose this is using quite a few clocks:
upperByte = commandNibble | ((outputDAC & 0x7F00)>>8 ) ;
Maybe find a way to do that in parallel with the nops?

Perhaps going back to SPI.transfer(), so it can be done while the transfer is waiting for the SPI.transfer interrupt would actually be a time savings in this case.

Get a faster processor.

For example, something like this

#include <SPI.h>
#define nop asm volatile ("nop")

unsigned int outputDAC;
int value = 1;
byte commandNibble = 0x70;
byte upperByte;
byte lowerByte;

void setup() {
  SPI.begin();
  SPI.setClockDivider(SPI_CLOCK_DIV2);

  pinMode (5, OUTPUT);// pin LDAC/
  pinMode (10, OUTPUT);//pin CS/
  pinMode (11, OUTPUT);//pin MOSI or SDI
  pinMode (13, OUTPUT);// SCK Clock
  Serial.begin(230400);
}

void loop() {
  while (1) {
    PORTB = PORTB & 0xFB;
    SPI.transfer (upperByte);
    lowerByte = outputDAC & 0x00FF;
    SPI.transfer (lowerByte);
    upperByte = commandNibble | ((outputDAC & 0x7F00) >> 8  ) ;
    PORTB = PORTB | 0x04;

    outputDAC = outputDAC + value;
    if (outputDAC == 0xFFF) {
      value = -1;
     // Serial.println (micros() );
    }
    if (outputDAC == 0) {
      value = 0xFFF;
    }
  }   // end while
}     // end loop

Not an SPI expert, but I was told in another post that "SPI.setClockDivider" got depreciated.

Joining two bytes first, and then writing the unsigned int with "SPI.transfer16" seemed faster in my tests.
Leo..

I was a little bit slow to answer you because this morning I was playing with my lawn mower.

I just scoped the last version you offered me and I put the same position on a sheet named "oscillogrammes 22 juillet".

in order to easily compare the scope pictures of the July 20 and 22 versions. (Annexed July 20 sheet named "timing MCP4922_FAST".

You will see that the version of July 20 is still much faster. (The July 20th 'program is included in the directory EXAMPLE of the zipped file posted July 20th. It was using a library that I have modified and which is included too in the zipped file)

I really learned a lot of things, thanks to your help but I conclude it was impossible with the MCP4922 (coupled with an Arduino Uno running 16MHZ) to work at a frequency above 70Hz even using only one of the two units provided in the chip.

I'll try to wire a 12 bit LTC1450 parallel DAC to an Arduino Uno in order to figure out which max speed may be reached. I have already used this circuit in a TTL assy and it can works at 5 kHz without any problem. I have also tried to work successfully at 20 kHz. A standalone ATMega328 coupled with LTC1450 could do the job, as soon as the breadboard will operate I'l came back to ask you some good ideas.

To resume, I think the man who said it was possible to work at 25 kHz with a MCP4922 is a gentle dreamer...

Have a nice day.

timing MCP4922 _FAST.pdf (64.1 KB)

oscillogrammes 22 juillet.pdf (40.9 KB)

Very surprising and very disappointing experience (until now...) with a 12-bit parallel DAC.

I wired an LTC1450 DAC on a breadboard connected to an Arduino UNO.
I expected to get a sawtooth with a period of less than 2 msec and I have been very surprised to find that its period is> longer than 15 msec!

On the oscillogram you will note the sawtooth and the squared signal arriving on the LTC1450 at pin 15.
The sawtooth period is 7.6x2 = 15.2 msec.

This means that the fastest signal arriving on pin 4 of the DAC is 2048 times faster, i.e. 7.4 usec.
this 7.4 usec signal represents the time of one loop ...

The objective is to be able to significantly improve the speed of this loop!

Is it possible in C++ or may we look for an Assembler issue and how to implement it in C++?

sketch_jul23_lt1450_DAC_rev1.ino (685 Bytes)

LTC1450 waveform.pdf (11.3 KB)

1450 & Arduino UNO.pdf (30.9 KB)

14500lf p1&2.pdf (318 KB)

The LTC1450 is a micropower DAC, its slow because its low power. Settling time is perhas 5µs or more
from what I can make out.

The thing you haven't been addressing is what sampling rate you want.

If you expect 12 bit resolution with a 4096 steps in the waveform, then you need about 20MSPS
to get 5kHz wave.

But that's not how sampling systems work. You choose a sampling rate, say 500kSPS, and interpolate
the waveform. At 500kSPS you get 100 points per waveform for 5kHz. That doesn't affect
the accuracy at all (so long as you only look at frequencies less than half the sampling rate).

Pease find herewith an example of a LTC1450 drived by à HCT4040 (which play the role of AD+1 generator ).
If you drive the HCT4040 with a 1MHz TTL square clock: you obtain a 4 msec 12 bits Sawtooth at the LTC1450 output pin 21.

In place of the external generator you may, for example, connect a 1MHz clock...to obtain same result but I expect to be possible to drive the LTC1450 (in previous schematic example) with C++ (calling an Assembler routine) and which gives 2msec 12bit sawtooth...

The only purpose of this max simplified schematic is to try to reach max output sawtooth fcy but I have drived 4040 with such a schematics up to 20MHz.

LT1450 & CD4040 SAWTOOTH .pdf (24.5 KB)

just changing the schematics a little to avoid shift left and right but using port D1(TXD) and D2(RXD).

The increased speed is remarquable since now the period is a little bit more than 3msec.

I'm not sure that speed may be increased...

If any idea to increase speed: you are welcome.

sketch_jul23_lt1450_DAC_new_cablage.ino (571 Bytes)

LT1450 & Arduino UNO_new_cablage.pdf (31.8 KB)

Finally I'm turning back to Assembler to speed up as fast as possible the sawtooth using exactly the same wiring.

My last assembler experience dating of 1985 with Z80...

With the program below I' m able to drive the 12 bits sawtooth with a Arduino Uno @ +- 1KHz.

.INCLUDE "m328pdef.inc"
.org 0x0000
rjmp start

start:
ldi r16,0x3f
out DDRB,r16
ldi r16,0xff
out DDRD,r16
loop0:
ldi r17,0xf0
ldi r16,0
loop1:
out portb,r17
loop2:
out portd,r16
inc r16
brne loop2
inc r17
brne loop1

rjmp loop0
.EXIT

I dont'use LDAC/ level because the target was only speed.

This assembler coding is almost the image of the C++ used in previous version:
*/

void setup() {
DDRB=0X3F;//contains LTC1450 4 upper digits of AdCount
DDRD=0XFF;//contains LTC1450 8 lower digits of AdCount
}

void loop() {

for (PORTD=0;PORTD<256;PORTD++){

if (PORTD==255){PORTB++;}

if (PORTB>15){PORTD=0; PORTB=0;}
}
}

Please find herewith the sawtooth timing with C++, Both C and Assembler and assembler alone.
You'll note than the signal width with C++ is greater than 3.5 msec and with Assembler this period shut down to a little bit more than 1msec.

I suppose I have reached the limit of a 12 bit DAC wired in parallel mode with an Arduino Uno 16 Mhz clocked.

Thanks for the help received and have a nice day.