Arduino Due Vector Graphics Display

Here's a project I put together showing how the Arduino Due can be used to create a vector graphics display without the need for any additional hardware, apart from a PS2 controller which is used for input. 3 different demos are shown including a 3D point cloud, 2D clock and 3D wire frame image.

https://youtu.be/Ws91qKhglXY

Super nice demo :)

Wow! I can’t think of what I could do with it, but its still impressive!

-jim lee

Love it!

How do you blank the trace between points?

No need to blank the trace between points as long as the skew between the 2 DAC outputs can be made sufficiently small compared with the time the beam spends at each point. Unfortunately, the SAM3X8E doesn't appear to support simultaneous switching of the 2 outputs, but with careful programming it can be reduced to about 600 ns (use tag mode and full-word write as described in section 44 of the Atmel datasheet).

While there's no need to blank the beam between points, it is necessary to park it off-screen in between each frame while communicating with the PS2 controller in order to avoid creating a bright spot. The horizontal and vertical sensitivity of the scope are adjusted so that the full range of the DAC output is slightly larger than the screen and the beam is parked at 0, 0 just outside the lower left corner. The blanking interval can also be seen on the digital scope displaying voltage vs. time.

Hope that helps!

There is a tutorial to draw a clock on a scope with a DUE. However, I can't understand why hardware is added between the DUE and the scope unlike caipirinha project: https://www.nutsvolts.com/magazine/article/the-arduino-graphics-interface-part-1

I guess the code flow for the human head and the PS2 joystick could be: Fill a buffer for DAC0 with N points representing the human head for coordinates X Fill a buffer for DAC1 with N points representing the human head for coordinates Y Initialize DAC peripheral so that DAC outputs spit at 30 Hz * N (since you said your camera logs a frame every 30 Hz). Note that 30 * N can't exceed 1 million Hz (the maximum DAC frequency). Start a Timer Counter e.g. TC0 channel 2 to trigger internally the DAC peripheral with TIOA2. Start PDC DMA for DAC to output DAC buffers paced by TIOA2 outputs. For each trigger of DAC Handler, inside the DAC Handler, use the last PS2 commands to process DAC0 and DAC1 buffers accordingly (rotations around X, Y and Z axis with CMSIS-DSP Matrix functions for Cortex M3). DAC Handler is triggered every 33.3 ms (1s/30Hz = 33.3 ms), i.e. enough to do the math for X,Y and Z rotations if N not too big. The "up to date" buffers are now used by DAC0 and DAC1 thru the DMA. The major interest of DMA is that it uses no CPU clock cycle outside the DAC Handler. I.e. outside the DAC Handler, PS2 commands can be read in parallel in loop() to be used later inside the DAC Handler.

I saw the Nuts and Volts article too and wondered about that. Based on the diagram in their article, their method of programming results in a skew between the DACs of about 1.5 us which is then realigned using external sample and hold circuitry. I was able to get the skew down to 600 ns through programming, allowing me to effectively ignore it.

If you look carefully in the lower right hand corner of my clock, a small shadow is visible which is a direct result of this skew (when one DAC switches before the other, an incorrect XY address is produced which persists for the duration of the skew). Of course, no skew at all would be better, but I wouldn't bother building a whole board to do it, especially for what is basically just a novelty. If anyone out there knows how to get the 2 DAC outputs to switch simultaneously through programming alone, please feel free to chime in.

Note that this demo is entirely software controlled and does not make use of DMA. The time required for the software to calculate each pixel (plus a small amount of padding) provides sufficient delay at each location. Of course, I'm sure the use of DMA could help, but it wasn't needed for any of the images in this demo.

I have no better solution to output DAC0 and DAC1 simultaneously than setting the TAG_EN bit and Word_Word bit in DACC_MR as you did. I can only add MAXS bit in DACC_MR to reduce the conversion time from 25 clock cycles to 23 clock cycles( ~ 275 ns).

An example sketch I wrote some time ago to output the same sine wave on both DACs with a PDC DMA (I have added comments in case someone would want to give it a try for this particuliar project):

/***********************************************************************************/
/*  DAC0 and DAC1 output of a sin wave - Frequency of sine = 25 KHz                */
/***********************************************************************************/

// Output the same 25KHz sinewave on both DACs

const uint32_t sinsize  = 42 ;   // Size of buffer
const uint8_t TC_RC = 40;
const uint8_t TC_RA = TC_RC / 2;
uint32_t sinus[2][sinsize];

volatile uint32_t bufn;

void dac_setup () {

  PMC->PMC_PCER1 = PMC_PCER1_PID38;     // DACC power ON
  DACC->DACC_CR = DACC_CR_SWRST ;       // Reset DACC

  DACC->DACC_MR = DACC_MR_TRGEN_EN                   // Hardware trigger select
                  | DACC_MR_TRGSEL(0b011)            // Trigger by TIOA2
                  | DACC_MR_TAG_EN                   // enable TAG to set channel in CDR
                  | DACC_MR_WORD_WORD                // write to both channels
                  | DACC_MR_REFRESH (1)              // depends on what is expected, should be tuned
                  | DACC_MR_STARTUP_8
                  | DACC_MR_MAXS;

  DACC->DACC_IER |= DACC_IER_TXBUFE;                 // Interrupt used by PDC DMA
  
    DACC->DACC_ACR = DACC_ACR_IBCTLCH0(0b10)           // For DAC frequency > 500 KHz
                     | DACC_ACR_IBCTLCH1(0b10)
                     | DACC_ACR_IBCTLDACCORE(0b01);
  
  NVIC_EnableIRQ(DACC_IRQn);                         // Enable DACC interrupt

  DACC->DACC_CHER = DACC_CHER_CH0                    // enable channel 0 = DAC0
                    | DACC_CHER_CH1;                 // enable channel 1 = DAC1

  /*************   configure PDC/DMA  for DAC *******************/

  DACC->DACC_TPR  = (uint32_t)sinus[0];         // DMA buffer
  DACC->DACC_TCR  = sinsize;
  DACC->DACC_TNPR = (uint32_t)sinus[1];         // next DMA buffer (circular buffer)
  DACC->DACC_TNCR = sinsize;
  bufn = 1;
  DACC->DACC_PTCR = DACC_PTCR_TXTEN;            // Enable PDC Transmit channel request

}

void DACC_Handler() {

  // Dac Handler is triggered at 42MHz/TC_RC/sinsize = 25 KHz
  
  //const uint32_t CountMax = F_CPU / 2 * (sinsize * TC_RC);

  /******  This part for debugging only  *******/
  static uint32_t Count;
  Count++;
  if (Count == 25000) {
    Count = 0;
    PIOB->PIO_ODSR ^= PIO_ODSR_P27;  // Toggle LED_BUILTIN every 1 second
  }
/***********************************************/

/*  processing of DAC0 and DAC1 Buffers according to PS2 commands could be done here  */

/**********************************************/
  uint32_t status = DACC->DACC_ISR;   // Read and save DAC status register
  if (status & DACC_ISR_TXBUFE) {     // move DMA pointer to next buffer
    bufn = (bufn + 1) & 1;
    DACC->DACC_TNPR = (uint32_t)sinus[bufn];
    DACC->DACC_TNCR = sinsize;
  }
}

void tc_setup() {

  // TIOA2 internally triggers DACs outputs
  PMC->PMC_PCER0 |= PMC_PCER0_PID29;                      // TC2 power ON : Timer Counter 0 channel 2 IS TC2
  TC0->TC_CHANNEL[2].TC_CMR = TC_CMR_TCCLKS_TIMER_CLOCK1  // MCK/2, clk on rising edge
                              | TC_CMR_WAVE               // Waveform mode
                              | TC_CMR_WAVSEL_UP_RC        // UP mode with automatic trigger on RC Compare
                              | TC_CMR_ACPA_CLEAR          // Clear TIOA2 on RA compare match
                              | TC_CMR_ACPC_SET;           // Set TIOA2 on RC compare match


  TC0->TC_CHANNEL[2].TC_RC = TC_RC;  //<*********************  Frequency = (Mck/2)/TC_RC
  TC0->TC_CHANNEL[2].TC_RA = TC_RA;  //<********************   Any Duty cycle in between 1 and TC_RC

  TC0->TC_CHANNEL[2].TC_CCR = TC_CCR_SWTRG | TC_CCR_CLKEN; // Software trigger TC2 counter and enable
}

void setup() {

  PMC->PMC_PCER0 |= PMC_PCER0_PID12;  // PIOB power ON
  PIOB->PIO_OER |= PIO_OER_P27;       // Output enable on LED_BUILTIN

  // Fill sinus[] buffers

  /* Initial Buffers for DAC0 and DAC1 could be filled here */
  
  for (int i = 0; i < sinsize; i++)
  {
    uint32_t chsel = (0 << 12) | (1 << 28);                  // LSB on DAC0, MSB on DAC1 !!
    sinus[0][i]  = 2047 * sin(i * 2 * PI / sinsize) + 2047;  //  0 < sinus [i] < 4096
    sinus[1][i] = sinus[0][i] |= sinus[0][i] << 16 | chsel;  // two buffers formated
    // MSB [31:16]on channel 1
    // LSB [15:0] on chanel 0
  }

  // Fire !
  tc_setup();
  dac_setup();
}

void loop() {

/*  Permanent reading of PS2 commands could be done here  */
}

It’s too bad Atmel didn’t support simultaneous switching, but I guess that would have added cost. The controller has a single DAC core which is demuxed between the 2 outputs using sample and hold circuits. They would have had to add a second core, or additional sample and hold circuitry which is messy and would limit performance. Oh well, I guess we can’t have everything.