Calcolo dei cicli macchina in interrupt

Ho la necessità di generare degli impulsi a frequenza variabile (decisa da knob_1) in modo del tutto trasparente al programma in esecuzione. Ho deciso di utilizzare il timer1 di Arduino UNO e gestire la produzione dei fronti nell'interrupt del timer1. La frequenza degli impulsi dovrebbe variare tra i 50 Hz e i 2500Hz.
Ho impostato il timer a circa 10uS ma, utilizzando Arduino IDE, non ho la possibilità di sapere a priori quanti cicli macchina vengono utilizzati nella routine dell'interrupt che uso. Il SW di prova è il seguente:

#define coeff_1 1;
#define interc_1 10;
unsigned int count_1=0, dt_1=0, t_1;
unsigned int knob_1 = analogRead(A0);

void setup() {
  pinMode(13, OUTPUT); 
  TCCR1A = 0;
  TCCR1B = 0
  TCCR1B |= 0xb00000001;          // 16000000 / 1 = 16 Mhz = 62,5 nS
  TCNT1 = 0xFF60;                          //Imposto circa 10 uS
  TIMSK1 |= (1 << TOIE1);
}
void loop() {
  digitalWrite(13, LOW);
  knob_1 = analogRead(A0) * coeff_1 + interc_1;
}

ISR(TIMER1_OVF_vect) {
  TCNT1 = 0xFF60;                 //Imposto circa 10 uS
  if ((count_1 - t_1) > knob_1) {
     digitalWrite(13, HIGH);
     digitalWrite(13, LOW);    //Impulso da 3uS
    t_1 = count_1;		        //Ricarico il Timer  
  }
    count_1++;
  } 

Qualcuno ha idea di come calcolare i cicli macchina di una routine come quella di gestione dell'interrupt?
Vi sono altri modi per misurare i uS diversi da quello che mi sono inventato io (usare il contatore count_1 come se fosse millis() )?
Grazie per le eventuali risposte.
'73 de iw2fnd, Lucio.

In genere quando occorre farsi una idea dei cicli CPU impiegati per computare una routine si disassembla il file binario, ottenendo un file ascii contenete le istruzioni assembly. Su architettura AVR la maggior parte delle istruzioni richiede 1 o 2 colpi di clock, su architettura ARM è più complesso. Io posso recuperare il comando avr-objdump, ma la soluzione più pratica per osservare il disassemblato è quella di usare wokwi. Con wokwi click pulsante destro del mouse per fare comparire il menu contestuale e scegliere Command Palette, selezionare l'opzione View Compiled Assembly Code Listing si apre il TAB sketch.lst. Di seguito il contenuto iniziale:


sketch.ino.elf:     file format elf32-avr


Disassembly of section .text:

00000000 <__vectors>:
   0:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__ctors_end>
   4:	0c 94 8a 00 	jmp	0x114	; 0x114 <__bad_interrupt>
   8:	0c 94 8a 00 	jmp	0x114	; 0x114 <__bad_interrupt>
   c:	0c 94 8a 00 	jmp	0x114	; 0x114 <__bad_interrupt>
  10:	0c 94 8a 00 	jmp	0x114	; 0x114 <__bad_interrupt>
  14:	0c 94 8a 00 	jmp	0x114	; 0x114 <__bad_interrupt>
  18:	0c 94 8a 00 	jmp	0x114	; 0x114 <__bad_interrupt>

Sempre con wokwi (o anche dal vero) si può usare il componente Analizzatore Logico che tramite programma sul PC permette di vedere i segnali dei pin. Ad esempio vuoi vedere quanto tempo impiega ad eseguire la funzione loop(), all'inizio accendi un pin e alla fine lo spegni.

Ho potuto verificare che wokwi è incredibilmente fedele alla realtà specie per quanto riguarda le tempistiche rilevate tramite l'analizzatore logico.

Ciao.

Grazie Maurotec.
in effetti fare reverse engineering del codice macchina è una buona idea. Non conosco il SW che mi proponi ma ci darò un'occhiata.
L'alternativa è scrivere in assembler la routine di risposta all'interrupt del timer1. Mi pare d'aver letto che si possa incapsulare una parte in assembler Atmega328P.
Ad ogni modo grazie per avermi risposto e per la buona idea.
73 de iw2fnd Lucio

Non sono certo al 100%, ma ricordo che il timer1 è a 16-bit e in passato l'ho usato per generato onde quadre a frequenza variabile senza consumare un ciclo di clock cpu. Io sinceramente non ho proprio voglia di aprire il datasheet del 328, ma tu hai necessità, per cui ti consiglio di leggere per bene la parte dedicata ai timer0, 1 e 2 e il loro prescaler. Tutti e tre i timer possono generare PWM e onde quadre con dcycle del 50%. Ho letto rapidamente vecchi appunti miei e credo che la modalità CTC sia quella migliore. C'è da vedere se si riesce a coprire il range 50Hz ÷ 2500Hz con un solo valore di prescaler (sarebbe il divisore del clock che alimenta il timer).

Senza scomodare l'assembly si può ottimizzare la ISR non usando digitalWrite() che consuma parecchi cicli cpu, ma usando i registri DDR e PORT che richiedono un solo ciclo di clock per essere eseguiti. Sempre in C è possibile evitare di eseguire prologo ed epilogo della ISR. Prologo ed epilogo sono iniettati dal compilatore e in genere salvano registri r0÷r31, ma potrebbero anche essere già vuoti, l'unico modo è verificare osservando il disassemblato per capire come intervenire. Per l'assembly puoi anche inserirlo inline ma la sintassi è tremenda. C'è anche la possibilità di creare un file con estensione .s contenente codice assembly, l'ide di arduino dovrebbe (ricordo che lo faceva) riconoscere l'estensione e usare la giusta ricetta per trasformare il .s in .o binario. Il tutto viene "linkato" in un solo file .elf da cui viene ricavato il file .hex che è il firmware trasferito nella flash.
Per trovare questi file devi sicuramente abilitare nell'ide la modalità verbose (non ricordo come si fa) nelle impostazioni, così da ottenere nel output maggiori informazioni, il fine ultimo è ricavare la cartella temporanea usata per lo specifico progetto.

Ciao.

Ottima anche l'idea di fare un'operazione sui bit della porta per risparmiare cicli macchina. Questa mi sembra la via più percorribile. Proverò.
Grazie.

Tanto per curiosità ho un progetto aperto su wokwi e ho potuto copiare il tuo codice con la ISR e prologo ed epilogo sono pesanti per la cpu. Vedi push all'inizio per salvare SREG e poi pop alla fine, push spinge i dati nello stack e pop li tira su, in modo che alla fine della ISR i registri r0÷r31 abbiano lo stesso valore che avevano prima che venisse eseguita la ISR.

ISR(TIMER1_OVF_vect) {
     78c:	1f 92       	push	r1
     78e:	0f 92       	push	r0
     790:	0f b6       	in	r0, 0x3f	; 63
     792:	0f 92       	push	r0
     794:	11 24       	eor	r1, r1
     796:	2f 93       	push	r18
     798:	3f 93       	push	r19
     79a:	4f 93       	push	r20
     79c:	5f 93       	push	r21
     79e:	6f 93       	push	r22
     7a0:	7f 93       	push	r23
     7a2:	8f 93       	push	r24
     7a4:	9f 93       	push	r25
     7a6:	af 93       	push	r26
     7a8:	bf 93       	push	r27
     7aa:	cf 93       	push	r28
     7ac:	df 93       	push	r29
     7ae:	ef 93       	push	r30
     7b0:	ff 93       	push	r31
  TCNT1 = 0xFF60;                 //Imposto circa 10 uS
     7b2:	80 e6       	ldi	r24, 0x60	; 96
     7b4:	9f ef       	ldi	r25, 0xFF	; 255
     7b6:	90 93 85 00 	sts	0x0085, r25	; 0x800085 <__DATA_REGION_ORIGIN__+0x25>
     7ba:	80 93 84 00 	sts	0x0084, r24	; 0x800084 <__DATA_REGION_ORIGIN__+0x24>
  if ((count_1 - t_1) > knob_1) {
     7be:	c0 91 72 02 	lds	r28, 0x0272	; 0x800272 <count_1>
     7c2:	d0 91 73 02 	lds	r29, 0x0273	; 0x800273 <count_1+0x1>
     7c6:	80 91 70 02 	lds	r24, 0x0270	; 0x800270 <__data_end>
     7ca:	90 91 71 02 	lds	r25, 0x0271	; 0x800271 <__data_end+0x1>
     7ce:	9e 01       	movw	r18, r28
     7d0:	28 1b       	sub	r18, r24
     7d2:	39 0b       	sbc	r19, r25
     7d4:	80 91 36 03 	lds	r24, 0x0336	; 0x800336 <knob_1>
     7d8:	90 91 37 03 	lds	r25, 0x0337	; 0x800337 <knob_1+0x1>
     7dc:	82 17       	cp	r24, r18
     7de:	93 07       	cpc	r25, r19
     7e0:	60 f4       	brcc	.+24     	; 0x7fa <__vector_13+0x6e>
     digitalWrite(13, HIGH);
     7e2:	61 e0       	ldi	r22, 0x01	; 1
     7e4:	8d e0       	ldi	r24, 0x0D	; 13
     7e6:	0e 94 de 00 	call	0x1bc	; 0x1bc <digitalWrite>
     digitalWrite(13, LOW);    //Impulso da 3uS
     7ea:	60 e0       	ldi	r22, 0x00	; 0
     7ec:	8d e0       	ldi	r24, 0x0D	; 13
     7ee:	0e 94 de 00 	call	0x1bc	; 0x1bc <digitalWrite>
    t_1 = count_1;		        //Ricarico il Timer  
     7f2:	d0 93 71 02 	sts	0x0271, r29	; 0x800271 <__data_end+0x1>
     7f6:	c0 93 70 02 	sts	0x0270, r28	; 0x800270 <__data_end>
  }
    count_1++;
     7fa:	21 96       	adiw	r28, 0x01	; 1
     7fc:	d0 93 73 02 	sts	0x0273, r29	; 0x800273 <count_1+0x1>
     800:	c0 93 72 02 	sts	0x0272, r28	; 0x800272 <count_1>
  } 
     804:	ff 91       	pop	r31
     806:	ef 91       	pop	r30
     808:	df 91       	pop	r29
     80a:	cf 91       	pop	r28
     80c:	bf 91       	pop	r27
     80e:	af 91       	pop	r26
     810:	9f 91       	pop	r25
     812:	8f 91       	pop	r24
     814:	7f 91       	pop	r23
     816:	6f 91       	pop	r22
     818:	5f 91       	pop	r21
     81a:	4f 91       	pop	r20
     81c:	3f 91       	pop	r19
     81e:	2f 91       	pop	r18
     820:	0f 90       	pop	r0
     822:	0f be       	out	0x3f, r0	; 63
     824:	0f 90       	pop	r0
     826:	1f 90       	pop	r1
     828:	18 95       	reti

Non considerando il peso di due digitalWrite(), solo prologo ed epilogo cosumano 32 x 2 clock = 64 x 2 = 128 clock 62.5 x 128 = 8000ns (8us).

Sempre se non sbaglio i calcoli, che però mi sembrano confermati dalla simulazione che consuma tanti cicli e segna 8%, contro soliti 28% con il mio vecchio PC 4 core. Segno che non fa a tempo ad eseguire la ISR che al termine deve rientrarci.

Nel manuale del 328 in fondo trovi il set di istruzioni mnemoniche e il loro consumo di clock.

Prego, sempre interessante parlare di queste cose con qualcuno che le capisce.
PS: (iw9ayt mio papà deceduto nel 95). Fu uno dei fondatori di una associazione radio amatori riconosciuta dalla protezione civile. Facevamo simulazioni di catastrofe, dove grazie a postazioni radio fisse e mobili aggiravamo la montagne, permettendo all'informazione di viaggiare rapidamente.

Ciao.

Non capisco che necessità ci sia di valutare i cicli di clock usando il Timer1: eseguendo il tuo codice nella ISR sei di fatto "svincolato" dal resto del contesto.

Io avrei usato un approccio diverso: invece di far andare il Timer1 di continuo e "usare il contatore count_1 come se fosse millis()", imposterei la frequenza con cui viene chiamata la ISR agendo sul prescaler e sul registro ICR1 del Timer1.

A quel punto nella ISR generi l'impulso senza fare altro e quindi non devi preoccuparti di quanto è lunga la tua funzione visto che il tempo di esecuzione sarà sempre costante.

Fatto al volo (dicendo per filo e per segno cosa fare a SkiatGPT perché sono pigro)

@cotestatnt
La creazione di una variabile simile a millis, ma più veloce, mi serve per poter gestire due frequenze d'impulsi autonome e controllare la velocità di due motori PP in modo indipendente.
Se il timer 1 fosse troppo veloce, per le istruzioni che deve eseguire la routine del suo interrupt, sarebbe sempre dentro la sua routine ed il processore non avrebbe tempo di fare altro.
@Maurotec
Ho trovato più semplice generare un impulso lungo quanto la routine di interrupt e vedere con l'oscilloscopio quanto questo impulso è lungo.
Tra le varie cose ho aggiunto anche una variabile di conteggio degli impulsi erogati che funge da variabile di posizione (integrale della velocità) per sapere quanta strada ha fatto il motore.
Così facendo normalmente la routine spende 15uS mentre quando fa tutte le elaborazioni ne spende 28uS. Ho impostato quindi il timer 1 a 40uS per avere margine per le istruzioni di interfaccia umana che sono molto più lente.
Il codice è diventato questo:

//#define Step 0xFE1F;    // Passo di 30uS
#define Step 0xFD7F;    // Passo di 40uS
//#define Step 0xFCDF;    // Passo di 50uS
#include <Wire.h>;

const int Clk_1=13, Enb_1=12, Dir_1=11, Pot_1=A0;
const int Clk_2=10, Enb_2=9, Dir_2=8, Pot_2=A1;

unsigned int count_1=0, t_1;
unsigned int t_2;
unsigned int knob_1 = analogRead(Pot_1);
unsigned int knob_2 = analogRead(Pot_2);
unsigned int posiz_1 = 0;   //Posizione Motore 1
unsigned int posiz_2 = 0;   //Posizione Motore 2

void setup() {
  pinMode(Clk_1, OUTPUT);    //Clock Motore1
  pinMode(Enb_1, OUTPUT);    //Enable Motore1
  pinMode(Dir_1, OUTPUT);    //Direction Motore1
  pinMode(Clk_2, OUTPUT);    //Clock Motore2
  pinMode(Enb_2, OUTPUT);    //Enable Motore2
  pinMode(Dir_2, OUTPUT);    //Direction Motore2

  digitalWrite(Clk_1, LOW);
  digitalWrite(Enb_1, HIGH);  //Motore 1 OFF
  digitalWrite(Dir_1, LOW);
  digitalWrite(Clk_2, LOW);
  digitalWrite(Enb_2, HIGH);  //Motore 2 OFF
  digitalWrite(Dir_2, LOW);

  Serial.begin(9600);
  
  TCCR1A = 0;
  TCCR1B = 0;

  //TCCR1B |= B00000101;  //1024
  // 16000000 / 1024 = 15 Khz
  //TCCR1B |= B00000100; //256
  // 16000000 / 256 = 62.5 Khz
  //TCCR1B |= B00000011; //64
  // 16000000 / 64 = 250 Khz
  //TCCR1B |= B00000010; //8
  // 16000000 / 8 = 2000 Khz
  TCCR1B |= B00000001; //1
  // 16000000 / 1 = 16 Mhz = 62,5 nS
  //TCCR1B |= B00000000; //Disabilitato

  TCNT1 = Step;   //Imposto il passo

  TIMSK1 |= (1 << TOIE1);
}

void loop() {
  
  knob_1 = map( analogRead(Pot_1), 0, 1024, 8, 50 );
  knob_2 = map( analogRead(Pot_1), 0, 1024, 10, 100 );
  //Serial.println(knob_1);
  //Serial.println(knob_2);
}
// **************** Routine di servizio all'interrupt del timer 1 (28uS max)
ISR(TIMER1_OVF_vect) {
  TCNT1 = Step;          //Ricarico il passo
// **************** Motore 1
  if ((count_1 - t_1) > knob_1) {
    digitalWrite(Clk_1, HIGH);
    digitalWrite(Clk_1, LOW); //Impulso da 3uS
    t_1 = count_1;		        //Ricarico il Timer  
    if (digitalRead(Enb_1) == LOW) {   
     if (digitalRead(Dir_1) == LOW) {
        posiz_1++;    //Incremento se direzione avanti
      } else {
        posiz_1--;    //Decremento se direzione indietro
      }
    }
  }
// **************** Motore 2
  if ((count_1 - t_2) > knob_2) {
    digitalWrite(Clk_2, HIGH);
    digitalWrite(Clk_2, LOW);   //Impulso da 3uS
    t_2 = count_1;		        //Ricarico il Timer  
    if (digitalRead (Enb_2) == LOW) {   
     if (digitalRead (Dir_2) == LOW) {
        posiz_2++;    //Incremento se direzione avanti
      } else {
        posiz_2--;    //Decremento se direzione indietro
      }
    }  
  }
    count_1++;
  }

Ora posso dedicarmi all'interfaccia umana: pulsanti e display.
'73 de iw2fnd, Lucio.

Ok, questo ha senso.
Per quanto riguarda la ISR che "impegna" in modo eccessivo il micro, con le frequenze in gioco è una possibilità che non esiste. A 2500 Hz la ISR verrebbe richiamata ogni 400uS ed avrebbe una durata di circa 8uS (usando la manipolazione diretta delle porte si potrebbe arrivare anche a circa 1uS).

Faccio presente che 2500 passi al secondo senza rampa vanno bene per un drive a 1/8 di passo (motore da 200 passi/giro) ma è troppo veloce per un pilotaggio a mezzo passo.

Dipende da tipo motore, tipo driver, tensione alimentazione ma di regola
dai 120RPM in su bisogna usare sempre la rampa.

1 Like

@icio
Verissimo, ma non utilizzo 2500 passi sul motore. La velocità di rotazione viene decisa dalla variabile knob_X che viene mappata a seconda del range di velocità che uno sceglie.
Al momento piloto il motore a 1/2 passo, sarebbe meglio 1/4 per non avere troppe vibrazioni. Le vibrazioni sono disastrose considerando che l'azionamento dovrebbe essere montato su di una rettifica.
Stavo pensando anche di sostituire il motore PP dell'asse X con un motore in CC perchè sull'asse X di una rettifica ci sono i fine corsa. Sull'asse Y invece il PP va bene perchè l'asse Y si muove quando la mola è fuori dal pezzo.
L'azionamento di un motore CC richiede almeno un mezzo ponte con alimentazione duale o un ponte intero per l'inversione della velocità. Potrei fare con un L298.
73 de iw2fnd Lucio

Se le caratteristiche del motore PP lo consentono, ti consiglio i driver della Trinamic (credo che ora sia tutto Analog Devices) come ad esempio il TMC2209 o il TMC2130 dotati della tecnologia StealthChop che li rende molto silenziosi (in pratica interpolano internamente sempre a 1/256 di passo)

@contestatnt
Grazie della dritta, li ho già comprati su Amazon.
73 de iw2fnd Lucio

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.