Puis-je rendre delayMicroseconds plus précis?


8

J'essaie de mordre des données DMX et cela nécessite des impulsions 4us. N'ayant pas beaucoup de chance avec les résultats, je vérifie pour voir à quel point l'Arduino est capable de retarder ... Semble être assez terrible.

Voici un petit test rapide que j'ai fait:

unsigned long ptime;

void setup() {
  Serial.begin(9600);
}

void loop() {
  ptime = micros();
  delayMicroseconds(4);
  Serial.println(micros() - ptime);

  delay(1000); //just to keep the serial monitor from going nuts
}

Et les résultats:

8 4 8 4 4 4 4 4 8 8 8 8 4 8 8 4 4 8 4 4 4 8 8 8 4 8 4

J'ai été un peu choqué de voir à quel point sa précision est mauvaise. C'est le double du temps que je voulais retarder, mais ce n'est même pas cohérent avec l'endroit où je pouvais diviser par 2!

Puis-je faire quelque chose pour obtenir des résultats corrects et cohérents?


Pourquoi frappez-vous au lieu d'accéder directement à l'UART?
Ignacio Vazquez-Abrams

Je peux donc avoir plus d'une sortie.
bwoogie

Le Mega possède quatre UART. Même si vous en détenez un en permanence pour une programmation qui vous donne quand même trois univers.
Ignacio Vazquez-Abrams

Je teste juste avec le méga parce que c'est ce que j'ai actuellement disponible, le projet final aura un ATMEGA328
bwoogie

1
Le problème ici est de savoir comment vous mesurez.
Gerben

Réponses:


7

Comme expliqué dans les réponses précédentes, votre problème réel n'est pas la précision de delayMicroseconds(), mais plutôt la résolution de micros().

Cependant, pour répondre à votre question réelle , il existe une alternative plus précise à delayMicroseconds(): la fonction _delay_us()de l'AVR-libc est à cycle précis et, par exemple

_delay_us(1.125);

fait exactement ce qu'il dit. La principale mise en garde est que l'argument doit être une constante de temps de compilation. Vous devez le faire #include <util/delay.h>pour avoir accès à cette fonction.

Notez également que vous devez bloquer les interruptions si vous souhaitez tout type de retard précis.

Edit : Par exemple, si je devais générer une impulsion de 4 µs sur PD2 (broche 19 sur le Mega), je procéderais comme suit. Tout d'abord, notez que le code suivant

noInterrupts();
PORTD |= _BV(PD2);   // PD2 HIGH
PORTD &= ~_BV(PD2);  // PD2 LOW
interrupts();

génère une impulsion longue de 0,125 µs (2 cycles CPU), car c'est le temps qu'il faut pour exécuter l'instruction qui définit le port LOW. Ensuite, ajoutez simplement le temps manquant dans un délai:

noInterrupts();
PORTD |= _BV(PD2);   // PD2 HIGH
_delay_us(3.875);
PORTD &= ~_BV(PD2);  // PD2 LOW
interrupts();

et vous avez une largeur d'impulsion à cycle précis. Il convient de noter que cela ne peut pas être réalisé avec digitalWrite(), car un appel à cette fonction prend environ 5 µs.


3

Vos résultats de test sont trompeurs. delayMicroseconds()retarde en fait assez précisément (pour des retards de plus de 2 ou 3 microsecondes). Vous pouvez examiner son code source dans le fichier /usr/share/arduino/hardware/arduino/cores/arduino/wiring.c (sur un système Linux ou sur un chemin similaire sur d'autres systèmes).

Cependant, la résolution de micros()est de quatre microsecondes. (Voir, par exemple, la page garretlab surmicros() .) Par conséquent, vous ne verrez jamais une lecture entre 4 microsecondes et 8 microsecondes. Le retard réel peut être de quelques cycles sur 4 microsecondes, mais votre code le signalera comme 8.

Essayez de faire 10 ou 20 delayMicroseconds(4);appels consécutifs (en dupliquant le code, pas en utilisant une boucle), puis signalez le résultat de micros().


Avec 10 retards de 4, je reçois un mélange de 32 et 28 ... mais 4 * 10 = 40.
bwoogie

Qu'est-ce que vous obtenez avec, disons, 10 retards de 5? :) Notez également que pour le bitbang, vous devrez peut-être utiliser des accès de port assez directs, c'est-à-dire non digitalWrite(), ce qui prend plusieurs microsecondes à exécuter.
James Waldby - jwpat7

40 & 44 ... Mais cela ne devrait-il pas être arrondi? Qu'est-ce que j'oublie ici?
bwoogie

Par «arrondir», vous voulez dire delayMicroseconds()? Je ne vois pas cela mieux que d'arrondir. ¶ Concernant la source de l'inexactitude, si la routine est en ligne, le timing dépend du code environnant. Vous pouvez lire les listes de montage ou de démontage pour voir. (Voir la section «Créer des listes d'assemblages» dans ma réponse à la question Équivalent de PORTB dans Arduino Mega 2560 , qui peut de toute façon être pertinente pour votre projet de bitbanging
James Waldby - jwpat7

2

Je vérifie à quel point l'Arduino est capable de retarder ... Semble être assez terrible.

micros()a une résolution bien documentée de 4 µs.

Vous pouvez améliorer la résolution en modifiant le prédéfinisseur du minuteur 0 (bien sûr, cela fait sortir les chiffres, mais vous pouvez compenser cela).

Vous pouvez également utiliser la minuterie 1 ou la minuterie 2 avec un pré-échelle de 1, ce qui vous donne une résolution de 62,5 ns.


 Serial.begin(9600);

Ça va être lent de toute façon.


8 4 8 4 4 4 4 4 8 8 8 8 4 8 8 4 4 8 4 4 4 8 8 8 4 8 4

Votre sortie est exactement cohérente avec la résolution de 4 µs micros()couplée au fait que parfois vous obtiendrez deux "ticks" et parfois un, selon exactement quand vous avez commencé le timing.


Votre code est un exemple intéressant d'erreur de mesure. delayMicroseconds(4);retardera de près de 4 µs. Cependant, vos tentatives de mesure sont fautives.

De plus, si une interruption se produit, cela allongera un peu l'intervalle. Vous devez désactiver les interruptions si vous souhaitez un délai exact.


1

Mesuré avec un oscilloscope, j'ai trouvé que:

delayMicroseconds(0)= delayMicroseconds(1)= 4 μs de retard réel.

Donc, si vous voulez un délai de 35 μs, vous avez besoin de:

delayMicroseconds(31);

0

Puis-je faire quelque chose pour obtenir des résultats corrects et cohérents?

L'implémentation Arduino est assez générique et peut donc ne pas être aussi efficace dans certaines applications. Il existe plusieurs façons de réduire les retards, chacun avec ses propres déficits.

  1. Utilisez nop. Chacun est une instruction donc le 16e de nous.

  2. Utilisez tcnt0 directement. Chacun est 4us car le prescaler est réglé sur 64. Vous pouvez changer le prescaker pour atteindre la 16e résolution américaine.

  3. Utilisez des ticks, vous pouvez implémenter un clone de systick et l'utiliser comme base du retard. Il offre une résolution plus fine et une précision.

Éditer:

J'ai utilisé le bloc suivant pour chronométrer les différentes approches:

time0=TCNT0;
delay4us();             //65
//t0delayus(4*16);          //77
//_delay_us(4);             //65
//delayMicroseconds(4);     //45
time1=TCNT0 - time0;        //64 expected

avant cela, j'avais réinitialisé le prédécaleur timer0 à 1: 1, donc chaque tick TCNT0 est 1/16 de microseconde.

  1. delay4us () est construit à partir de NOP (); il a produit un retard de 65 ticks, soit un peu plus de 4us;
  2. t0delayus () est construit à partir des retards du timer0. il a produit un retard de 77 ticks; légèrement pire que delay4us ()
  3. _delay_us () est une fonction gcc-avr. performance à égalité avec delay4us ();
  4. delayMicroseconds () a produit un retard de 45 ticks. la façon dont arduino a implémenté ses fonctions de synchronisation, il a tendance à sous-compter, sauf dans un environnement avec d'autres interruptions.

J'espère que cela aide.


Notez que le résultat attendu est de 65 ticks, pas 64, car la lecture TCNT0prend 1 cycle CPU.
Edgar Bonet
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.