Precisión de señales analogas en DAC arduino DUE

Buenos días, de antemano quisiera decir que en el uso del arduino DUE soy algo novato.

Necesito generar una señal diente de sierra de frecuencia variable de entre; 10Hz a 100Hz con gran precisión para alimentar un sensor y su manejo en posteriores etapas.

Realice un código para generar la diente de sierra en base a una recta con pendiente variable, para que al variar la pendiente cambiase el tiempo que demora la recta al alcanzar cierto límite superior, luego de esto simplemente la llevaría a 0 dentro de un bucle muestreado en milisegundos con la función millis().

El tema es que eh tenido problemas con la precisión en la frecuencia de la señal al medirla en un osciloscopio la cual se desfasa en varios milisegundos, en verdad no sé hasta qué punto o de qué forma puedo mejorarlo, intente usar la función micros() pero no me dio resultado. Me recomedaron usar un reloj externo en este caso el DS3231 pero realmente no se si sea lo que necesito.

Mi consulta va en si existe alguna librería u otro método que me permita mejorar el muetreo de la señal para obtenerla de forma mas precisa.

De antemando muchas gracias
(El código lo implementé en Matlab y funciono sin ningun problema)

//---------------Variables muestreo----------------------//
unsigned long t0=0; //Tiempo inicial
unsigned long t=0; //Vector tiempo en [ms]
long ta=0; // Tiempo actual
int T=1; // Periodo de muestreo en [ms]
int limite=4095;
float y=0;
int m=4095/5000; //pendiente
long x=0;
void setup() {
analogWriteResolution(12);
}
void loop() {
  t=millis(); //Vector tiempo en [ms]
  ta=t-t0;    //Cambio de instante actual muestreo
  if (ta >= T){ //Condicion lectura en instante de muestreo
  analogWrite(DAC0,y);
  y=x*m;
  x=x+1;
  if (y>=limite)
  {x=0;
    }
  t0=t;} //reemplazo el valor pasado con el actual  
}

DienteSierra1.ino (610 Bytes)

cuando escribes:

int m = 4095/5000;

el resultado es m = 0, entonces Y = x * m = 0 :o

Me recomedaron usar un reloj externo en este caso el DS3231 pero realmente no se si sea lo que necesito.

Quien te recomendó esto esta mas perdido que tu.

Lo que necesitas es usar alguna Librería como TimerOne que te genera un timer y tu aprovechar ese timer para ir creando los pasos de tu diente de sierra.

Si tienes un DAC de 12 bit o 4096 pasos entonces una un tick del timer que te permita actualizar para obtener la frecuencia deseada o bien, varia el muestreo para adecuarlo a tu frecuencia final.
Eso te dará la precisión independientemente de lo que hagas en tu código.

Te sugiero esto porque hablas de precisión entonces olvida micros() cuando no sabes cuanto te consume cada instrucción que pusiste y si lo sabes y luego editas y cambias debes reconsiderarlo.
Con el uso de un TImer eso no ocurre.

ard_newbie:
cuando escribes:

int m = 4095/5000;

el resultado es m = 0, entonces Y = x * m = 0 :o

Tienes razón fue el ultimo valor que se guardo cuando probaba el código ayer, no me había fijado en cambiar el int por un float. Gracias por la indicación

surbyte:
Lo que necesitas es usar alguna Librería como TimerOne que te genera un timer y tu aprovechar ese timer para ir creando los pasos de tu diente de sierra.

Tengo entendido que el uso de esa librería implicaría usar interrupciones por relojes que se ejecutan de forma separada del programa principal, nunca lo eh usado pero déjame probar a ver como resulta al medir con el osciloscopio.

De todas maneras, crees que esto me pueda causar algún inconveniente en otra parte del programa donde tengo que usar la misma señal diente de sierra en conjunto con otra señal de un sensor que tengo que conectar, mas que nada para tenerlo presente.

Gracias por responder.

NikoUTA:
Tienes razón fue el ultimo valor que se guardo cuando probaba el código ayer, no me había fijado en cambiar el int por un float. Gracias por la indicación

El coeficiente m es incorrecto, debería ser 4095/3300. Además, en el DUE, el DAC sale entre 1/6 * 3.3V y 5/6 * 3.3V :

Otra solución es usar una señal PWM seguida de un filtro RC.

Y luego usa micros () en lugar de millis ().

De todas maneras, crees que esto me pueda causar algún inconveniente en otra parte del programa donde tengo que usar la misma señal diente de sierra en conjunto con otra señal de un sensor que tengo que conectar, mas que nada para tenerlo presente.

Que inconveniente? Y no se de que parte del programa hablas porque no esta publicdo asi que especular sobre algo que no se ve, me resulta imposible.

Me refería a que si el uso de las interrupciones me podría causar algún conflicto dentro de otras etapas del programa, como mencionaba antes que sería en la lectura de un sensor y el propio uso de la diente de sierra, que son partes que me faltan por diseñar.

Más que nada para tenerlo presente porque nunca las eh usado y tengo entendido que el micro prioriza su ejecución dentro del programa.

ard_newbie:
El coeficiente m es incorrecto, debería ser 4095/3300. Además, en el DUE, el DAC sale entre 1/6 * 3.3V y 5/6 * 3.3V :

ArduPicLab: How to modify analog output range of Arduino Due

Gracias por la info, la verdad es que me había dado cuenta del offset y del valor máximo por las mediciones dentro del osciloscopio pero no tenía claro los valores generales para cada uno.

El timer no te provocará problema alguno. Si lo hiciera habría ADVERTENCIAS por todas partes. Si encuentras alguna me avisas, dicho con ironía porque no las hay.

Trabaja tranquilo con TIMERs son maravillososo para casos como este.

Si, buscando encontré la librería duetimer para trabajar con los timer del due y tienes razón la señal mejora bastante en la precisión por periodo de cada rampa en comparación del uso de millis() o micros(). Y el tema de los conflictos, pueden darse pero usando otras librerías que usen los mismos registros, pero no siendo el caso no habría problema.

Una última cosa, me puedes orientar en el código para retornar los valores de la diente de sierra generados dentro de la función de la interrupción hacia el loop principal, para poder seguir trabajando con ellos que es la duda que me esta quedando.

(Cambie la forma de generar la diente de sierra usando ahora un vector de valores definidos)

#include <DueTimer.h>
int i=0;
float d=0;

float muestras[120]={0x22 , 0x44 , 0x66 , 0x88 , 0xaa , 0xcc , 0xee , 0x110 , 0x132 , 0x154 ,
0x176 , 0x198 , 0x1ba , 0x1dc , 0x1fe , 0x220 , 0x242 , 0x264 , 0x286 , 0x2a8 ,
0x2ca , 0x2ec , 0x30e , 0x330 , 0x352 , 0x374 , 0x396 , 0x3b8 , 0x3da , 0x3fc ,
0x41e , 0x440 , 0x462 , 0x484 , 0x4a6 , 0x4c8 , 0x4ea , 0x50c , 0x52e , 0x550 ,
0x572 , 0x594 , 0x5b6 , 0x5d8 , 0x5fa , 0x61c , 0x63e , 0x660 , 0x682 , 0x6a4 ,
0x6c6 , 0x6e8 , 0x70a , 0x72c , 0x74e , 0x770 , 0x792 , 0x7b4 , 0x7d6 , 0x7f8 ,
0x81a , 0x83c , 0x85e , 0x880 , 0x8a2 , 0x8c4 , 0x8e6 , 0x908 , 0x92a , 0x94c ,
0x96e , 0x990 , 0x9b2 , 0x9d4 , 0x9f6 , 0xa18 , 0xa3a , 0xa5c , 0xa7e , 0xaa0 ,
0xac2 , 0xae4 , 0xb06 , 0xb28 , 0xb4a , 0xb6c , 0xb8e , 0xbb0 , 0xbd2 , 0xbf4 ,
0xc16 , 0xc38 , 0xc5a , 0xc7c , 0xc9e , 0xcc0 , 0xce2 , 0xd04 , 0xd26 , 0xd48 ,
0xd6a , 0xd8c , 0xdae , 0xdd0 , 0xdf2 , 0xe14 , 0xe36 , 0xe58 , 0xe7a , 0xe9c ,
0xebe , 0xee0 , 0xf02 , 0xf24 , 0xf46 , 0xf68 , 0xf8a , 0xfac , 0xfce , 0xff0
};
void setup() {
 //Serial.begin(9600);
 analogWriteResolution(12);
 analogReadResolution(12);
 Timer1.attachInterrupt(sawtooth);
 //Timer1.setFrequency(1);
 Timer1.start(166.7); //llama la interrupcion en microseg
}

void loop() {

}

void sawtooth() {
  
  i++;
  d=muestras[i]; //se guarda el valor de cada elemento del vector
  analogWrite(DAC0,d);
  if (i>=120)
    {i=1;
      }
  }

Una última cosa, me puedes orientar en el código para retornar los valores de la diente de sierra generados dentro de la función de la interrupción hacia el loop principal, para poder seguir trabajando con ellos que es la duda que me esta quedando.

if (i>=120)
    {i=1;
      }

Esto debería ser

if (i>=120)
    i=0;

A esto te referieres?

Con un timer, el cambio de frecuencia es menos limpio que un PWM que cambiará al final de un período completo.

Un ejemplo de activación con PWM:

const uint8_t bufsize = 120;
uint16_t buf[bufsize];

uint32_t PERIOD_VALUE = 35000; // Between 3500---> 100 Hz and 35000 ---> 10 Hz

void setup() {

  for (uint8_t i = 0; i < bufsize; i++) {
    buf[i] = (uint16_t)(i * 4095 / bufsize);
  }
  dac_setup();
  pwm_setup();
}


void loop() {
  // Frequency of waveform  = MCK/2/CPRDUPD/bufsize
  // UPDate frequency with CPRDUPD, PERIOD_VALUE between 3500---> 100 Hz and 35000 ---> 10 Hz
  //PWM->PWM_CMP[0].PWM_CPRDUPD = PERIOD_VALUE;
}
/*************  Configure dac_setup function  *******************/
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(0b101)             // Trigger by PWM Event line 1
                  | DACC_MR_USER_SEL_CHANNEL0
                  | DACC_MR_REFRESH (1)
                  | DACC_MR_STARTUP_8
                  | DACC_MR_MAXS;

  DACC->DACC_ACR = DACC_ACR_IBCTLCH0(0b10)
                   | DACC_ACR_IBCTLCH1(0b10)
                   | DACC_ACR_IBCTLDACCORE(0b01);

  DACC->DACC_IDR = ~DACC_IDR_TXRDY;
  DACC->DACC_IER = DACC_IER_TXRDY;
  NVIC_EnableIRQ(DACC_IRQn);
  DACC->DACC_CHER = DACC_CHER_CH0;                    // enable channel0 = DAC0

}

void DACC_Handler() {
  static uint8_t Index;
  DACC->DACC_CDR = buf[Index];
  Index = (Index + 1) % bufsize;
}

void pwm_setup () {

  PMC->PMC_PCER1 |= PMC_PCER1_PID36;                   // PWM controller power on

  PWM->PWM_DIS = PWM_DIS_CHID0 ;                       // Disable PWM Reference channel 0

  // Set the PWM Reference channel 0 i.e. : Clock/Frequency/Alignment
  PWM->PWM_CLK = PWM_CLK_PREB(1) | PWM_CLK_DIVB(1);             // Set the PWM clock rate : MCK/2 = 42000000
  PWM->PWM_CH_NUM[0].PWM_CMR = PWM_CMR_CPRE_CLKB;               // The period is left aligned, clock source as CLKB on channel 0
  PWM->PWM_CH_NUM[0].PWM_CPRD = PERIOD_VALUE;                   // Set the PWM frequency Mck/CPRD/DIVB/PREB = F ;

  PWM->PWM_CMP[0].PWM_CMPV = PWM_CMPV_CV(50);                   //Define the comparison value in channel 0 to be compared with the counter of the channel 0.

  PWM->PWM_CMP[0].PWM_CMPM = PWM_CMPM_CEN;                      // Comparaison enable

  PWM->PWM_ELMR[1] = PWM_ELMR_CSEL0;                            // Event line 1 trigger according to CMPV of channel 0

  PWM->PWM_ENA = PWM_ENA_CHID0;                                 // Fire !!

}

Necesito generar una señal diente de sierra de frecuencia variable de entre; 10Hz a 100Hz con gran precisión para alimentar un sensor y su manejo en posteriores etapas.

Cuando hablas de variar lo dices en forma progresiva, hay una cadencia determinada para pasar de 100 a 10 o es un salto, y en ese caso cual es el problema?

No no, me refería a el como retornar los valores guardados en la variable "d" que está dentro de la función de la interrupción, hacia el loop principal o a otra función.

 void sawtooth() {
  i++;
  d=muestras[i]; //se guarda el valor de cada elemento del vector
  analogWrite(DAC0,d);
  if (i>=120)
    {i=1;
      }
  }

surbyte:
Cuando hablas de variar lo dices en forma progresiva, hay una cadencia determinada para pasar de 100 a 10 o es un salto, y en ese caso cual es el problema?

No, solo necesito tener frecuencias fijas de la señal entre 10 y 100 Hz para realizar pruebas con cada una.

ard_newbie:
Con un timer, el cambio de frecuencia es menos limpio que un PWM que cambiará al final de un período completo.

Gracias por responder, a que te refieres cuando dices que el cambio es menos limpio?

has creado una señal con 120 muestras, cada muestras le dice a analogWrite() que salida generar en el DAC.
i es el índices y cuando llegas al final de período solo lo pones a 0 y vuelves a generar otro período.
Por eso decía que cambiando
Timer1.start(166.7); //llama la interrupcion en microseg
120 x 166.7 useg = 20000 useg = 20 mseg o sea una F de 1/T = 50 hz
cambias el período y lograras la frecuencia que sea.
Ahora no se porque haces esto
```

  • d=muestras[i]; //se guarda el valor de cada elemento del vector
      analogWrite(DAC0,d);*
    * *yo directamente lo usaría asi* *
    *  analogWrite(DAC0,muestras[i]);*
    * *si por alguna razón necesitas interactuar entre la interrupción y el loop la variable debe llevar la palabra volatile* *ejemplo* *Si definieras a d para ser de algún modo modificado en el loop y en la interrupción deberias definirlo asi* *
    volatile float d;
    ```
    No se si te respondo a tu consulta.

Si gracias, nombrando la variable como volatile no tuve ningún problema en el programa.

Una última consulta que me surgió en cuanto al trabajo con los timers. Estos pueden trabajar de manera paralela, refiriéndome a por ejemplo; tener 2 funciones de interrupción muestreadas a diferente periodo de muetreo.

void setup() {
 //Serial.begin(9600);
 analogWriteResolution(12);
 analogReadResolution(12);
 Timer1.attachInterrupt(sawtooth);
 Timer2.attachInterrupt(lectura);
 //Timer1.setFrequency(1);
 Timer1.start(166.7);
 Timer2.start(1000);
}

Cuando digo que deben trabajar de manera paralela me refiero a que necesito que interactúen entre si, ocupando variables de una función dentro de otra.

No entendí la pregunta?

Me refería a que si es posible trabajar con 2 interrupciones a diferente periodo de muestreo funcionando al mismo tiempo. Estuve averiguando y encontré que podría ser posible ya que la librería en si trabaja con diferentes registros del timer, pero al fin al cabo el micro esta hecho para trabajar en forma secuencial y no paralela como quiero.

Lo unico que me queda es ver si funciona bien al verlo como trabaja.