PiPico2 Frequenzzähler

#7948192
Lesenswert?

Ich will mit dem PiPico2 Frequenzen zählen. Das Ganze soll über die Periodendauermessung funktionieren, da ich eine möglichst schnelles Update der aktuellen Frequenz benötige.

Mein Testaufbau ist ein Frequenzgenerator mit 100kHz Rechteck an den Pin16 des PiPico geführt. So wie es aussieht, entstehen hier Fehler bis zu ca. 20%.

1
#include <Arduino.h>
2

3
volatile uint32_t DeltaT = 0;
4

5
#define TESTPIN 16
6

7
void myISR() {
8
  static uint64_t last_cycles = 0;
9
  uint64_t t=rp2040.getCycleCount64();
10
  DeltaT=t-last_cycles;
11
  last_cycles = t; 
12
}
13

14
void setup() {
15
  Serial.begin(115200);
16
  //while (!Serial) {}
17
  pinMode(LED_BUILTIN,OUTPUT);
18
}
19

20
void setup1()
21
{
22
  pinMode(TESTPIN, INPUT_PULLUP);
23
  attachInterrupt(digitalPinToInterrupt(TESTPIN), myISR, RISING);
24
}
25

26
void loop() {
27
  // read last_cycles (simple read; if strict atomicity needed use atomic ops)
28
  static bool state;
29
  uint64_t c = DeltaT;//last_cycles;
30
  uint64_t f = rp2040.f_cpu();               // CPU frequency in Hz
31
  double ns = (double)c * 1e9 / (double)f;   // cycles -> nanoseconds
32
  float fmeas=1/(ns*1e-9);
33
  Serial.println(fmeas);
34
  delay(20);
35

36
}
Angehängte Dateien:
#7948208
Lesenswert?

Christoph M. schrieb:

Ich will mit dem PiPico2 Frequenzen zählen. Das Ganze soll über die Periodendauermessung funktionieren, da ich eine möglichst schnelles Update der aktuellen Frequenz benötige.

Wenn es sowohl schnell, als auch genau und ohne CPU Belastung gehen soll, dann empfehle ich nachdrücklich den Schwenk auf die PIO. Der Pico2 läuft (default) mit 150MHz, dh. damit ist eine Auflösung von 6 2/3ns möglich. Wer glatte Zahlen mag, bei 200MHz──▶5ns.

#7948212
Lesenswert?

Michael P. schrieb:

Kenne den Pico nicht, aber brauchst du unbedingt Variable-Typen inkl. double und print?

Der Pico2 hat zwei FPUs, bei float Addition und Multiplikation innerhalb eines Taktzyklus, Division innerhalb max. acht (IIRC) Double braucht mit dem CoPro etwas länger, was aber an der Stelle völlig egal ist.

Ich könnte mir vorstellen, dass der ganze multiple Arduiono Überbau/Abstraktion einer präzisen Messung nicht unbedingt zuträglich ist.

#7948219
Lesenswert?

Michael P. (mipo) 08.10.2025 07:12

Kannst du den Fehler besser beschreiben?

Die Anzeige funktioniert ziemlich sicher. Das Diagramm zeigt eher Ausreißer als Jitter. Klar, dass es so was bisweilen gibt. Manchmal auch durch Übertragungsfehlern. Der Screenshot ist vom Plotter der Arduino-IDE und das funktioniert stabil. Die Ausreißer würde ich eher als Interrupt-Querschläger ansehen. Ich hätte da weniger erwartet, weil ich die Zeitmessung mit "setup1" ja auf den zweiten Kern des PiPico gelegt habe.

Norbert (der_norbert)

Wenn es sowohl schnell, als auch genau und ohne CPU Belastung gehen soll, dann empfehle ich nachdrücklich den Schwenk auf die PIO.

Da wäre die Frage, ob es überhaupt mit der hohen Auflösung geht. Auch die PIO muss ja ein paar paar Instruktionen durchführen. Vor allen Dingen scheint mir der Aufwand recht hoch: Laut

https://github.com/earlephilhower/arduino-pico/discussions/221

soll man den Online-PIO Assembler

https://wokwi.com/tools/pioasm

verwenden.

: Bearbeitet durch User
#7948222
Lesenswert?

Christoph M. schrieb:

Da wäre die Frage, ob es überhaupt mit der hohen Auflösung geht. Auch die PIO muss ja ein paar paar Instruktionen durchführen.

Zur ersten Frage: Ja, geht es. Die PIO arbeitet jede Instruktion innerhalb eines Taktzyklus ab. Zählen braucht eine, springen braucht eine. (Wenn man's sauber macht)

Da man aber zwölf Statemachines zur Verfügung hat, kann man getrost zwei davon zeitversetzt das gleiche messen lassen.

Ich selbst habe ganz sicher schon tausende Zeilen PIO-Assembler geschrieben (nicht übertrieben) und nicht einmal auf einen Online-Gehgips zurück greifen müssen. Zwei Hände voll Instruktionen stellen jetzt lernmäßig nicht die Eiger Nordwand in den Schatten (Sorry für's Wortspiel)

#7948250
Lesenswert?

Christoph M. schrieb:

So wie es aussieht, entstehen hier Fehler bis zu ca. 20%.

Das wundert nicht, da ohne richtige Synchronisierung von Eingangssignal und Referenztakt keine stabile Messungen möglich sind. Entweder verwendest Du externe D-FFs oder eine interne PIO + DMA. Intern habe ich eine Lösung für den RP2040 auch für Arduino-IDE, die aber beim PicoPi2 wegen vermutlich wieder veränderter Arduino-Version nicht richtig compiliert wird. Beispiele kannst Du hier in der Codesammlung finden.

#7948266
Lesenswert?

Yalu X. (yalu) (Moderator) 08.10.2025 09:38

Läuft die serielle Kommunikation über USB? Dann unterbrechen oder blockieren die USB-Interrupts sporadisch deinen GPIO-Interrupt, was dann zu ungenauen Zeitmessungen führt.

Ich habe das Programm noch mal umgeschrieben und Interrupt-frei gemacht. Es sind dann keine Ausreißer mehr sichtbar. Das Ganze läuft extra ohne alles auf dem zweiten Kern. Aber die maximale Abweichung liegt immer noch einiges über der anvisierten Nanosekundenauflösung (siehe Anhang, y-Achse: us).

1
/*
2
 * Periodendauermessung auf zweitem Kern
3
 * 
4
 * Abweichung ca. 0.7us => zu ungenau
5
 * 
6
 */
7
#include <Arduino.h>
8

9
volatile uint32_t DeltaT = 0;
10

11
#define TESTPIN 16
12

13
void setup() {
14
  Serial.begin(115200);
15
  pinMode(LED_BUILTIN, OUTPUT);
16
}
17

18
void setup1()
19
{
20
  pinMode(TESTPIN, INPUT_PULLUP);
21
}
22
void loop1()
23
{
24
  while (1)
25
  {
26
    static uint32_t oldPin = 0;
27
    if (gpio_get(TESTPIN) != oldPin)
28
    {
29
      static uint64_t last_cycles = 0;
30
      uint64_t t = rp2040.getCycleCount64();
31
      DeltaT = t - last_cycles;
32
      last_cycles = t;
33
      oldPin = gpio_get(TESTPIN);
34
    }
35
  }
36
}
37

38
void loop() {
39
  static bool state;
40
  uint64_t c;
41
  uint64_t f;
42
  float ns;
43
  float us;
44
  float minVal=1e6;
45
  float maxVal=0;
46
  f = rp2040.f_cpu();               // CPU frequency in Hz
47
    
48
  for (int n = 0; n < 100;n++)
49
  {
50
    c = DeltaT;
51
    ns = (double)c * 1e9 / (double)f;   // cycles -> nanoseconds
52
    us = ns/1000;
53
    Serial.println(us); //
54
    delay(20);
55
    if(minVal>ns)minVal=ns;
56
    if(maxVal<ns)maxVal=ns;
57
  }
58
  //Serial.println("max[ns]:"+String(maxVal));
59
  //Serial.println("min[ns]:"+String(minVal));
60
}

Intern habe ich eine Lösung für den RP2040 auch für Arduino-IDE, die aber beim PicoPi2 wegen vermutlich wieder veränderter Arduino-Version nicht richtig compiliert wird.

Ich verwende das Early-Hill-Power Framework, das sollte recht stabil sein und besser als das originale Arduino PiPico mit MBed: https://github.com/earlephilhower/arduino-pico
Dort kann man auch die nativen SDK-Funktionen benutzen (siehe oben im Code "gpio_get" statt digitalRead).
Wo genau kann ich deinen RP2040 Code finden?

: Bearbeitet durch User
#7948280
Lesenswert?

Christoph M. schrieb:

Wo genau kann ich deinen RP2040 Code finden?

Hier findest Du die Entwicklungsgeschichte zu meinen RP2040 Schaltungen: http://mino-elektronik.de/fmeter/fm_software.htm#bsp_RP2040

Bei der 'Luxusversion' mit TDC gibt es die Ausführungen für IAR-EWARM, Segger-IDE und Arduino, die nahezu identisch sind. Die Messungen laufen alle über Interrupts im Hintergrund. Das bedingt wegen der standardmäßig bei Arduino vorhandenen USB-Schnittstelle auf der einen Seite, daß die Interruptvektoren ins RAM gelegt werden müssen. Auf der anderen Seite muß die 'RP2040.h' der beiden anderen IDEs für Arduino passen und darf nicht mit den dortigen *.h-Dateien kollidieren.

Für Arduino funktionierte das mit IDE V 2.3.2 package "Raspberry Pi Pico 3.7.0". Bei neueren Versionen sind unverständlicherweise die *.h-Dateien geändert/getauscht worden. Mir war es dann zu blöd, diesen Änderungen hinterher zu programmieren.

Im Anhang findest Du meine unvollendete Version, die eigentlich noch um serielle Befehle und Speicherung von Konstanten im FLASH-Speicher ergänzt werden sollte. Zur Datenübertragung wird USB benutzt. DEF_F1_MESSZEIT (in ms) kannst Du ja auf den benötigten Wert verkleinern. Ich hoffe, daß die Kommentare schon zur Verion passen.

Angehängte Dateien:
Moderator #7948296
Lesenswert?

Christoph M. schrieb:

Ich habe das Programm noch mal umgeschrieben und Interrupt-frei gemacht. Es sind dann keine Ausreißer mehr sichtbar.

Sehr gut, dann lag es also tatsächlich an den Interrupts.

Aber die maximale Abweichung liegt immer noch einiges über der anvisierten Nanosekundenauflösung

Mit welche Taktfrequenz läuft der Pico2? Glaubst du wirklich, man könne damit 1ns auflösen?

(siehe Anhang, y-Achse: us).

Ich habe mal mit großer Anstrengung versucht, die Schwankungsbreite aus deinem maximal schlecht skalierten Diagramm herauszulesen und komme auf etwa ±46ns. Das ist besser als ich von einer Lösung mit Arduino ohne PIO erwartet hätte.

Besser geht es natürlich mit PIOs.

Beitrag #7948316 wurde vom Autor gelöscht.
#7948322
Lesenswert?

Mi N.

Hier findest Du die Entwicklungsgeschichte zu meinen RP2040 Schaltungen: http://mino-elektronik.de/fmeter/fm_software.htm#bsp_RP2040

Danke. Mit DMA, beeindruckend :-)

1
/*
2
 PIO0-SM0:
3
 Zeitzaehlung im x-Register mit SystemCoreClock/PIO_TIME_DIV (=4)
4
 und Ausgabe des Wertes bei negativer Flanke des Eingangssignals
5
*/
6
void init_PIO0(void)
7
{
8
// PIO0 aktivieren  
9
  RESETS_CLR->RESET = RESETS_RESET_pio0_Msk;
10
  while (!(RESETS->RESET_DONE & RESETS_RESET_pio0_Msk));
11

12
// FIN1_PIN f�r SM0 initialisieren
13
  IO_BANK0->GPIO2_CTRL = IO_BANK0_GPIO0_CTRL_FUNCSEL_sio_0; // GPIO-Eingang
14
  PADS_BANK0->GPIO2 = PADS_BANK0_GPIO0_IE_Msk |            
15
//                      PADS_BANK0_GPIO0_PUE_Msk |          // pullup
16
                      PADS_BANK0_GPIO0_SCHMITT_Msk;         //  + schmitttrigger
17
  
18
  PIO0->SM0_PINCTRL |= 1<<PIO0_SM0_PINCTRL_SIDESET_BASE_Pos | 1<<PIO0_SM0_PINCTRL_SET_BASE_Pos;
19
  PIO0->SM0_EXECCTRL = (31 << PIO0_SM0_EXECCTRL_WRAP_TOP_Pos) | 
20
                        (0 << PIO0_SM0_EXECCTRL_WRAP_BOTTOM_Pos) |
21
                        (FIN1_PIN  << PIO0_SM0_EXECCTRL_JMP_PIN_Pos);  // Pin f�r JMP
22
// ADR0  
23
  PIO0->INSTR_MEM0 = 0x0241;      // jmp x-- -> ADR1, 2 x delay
24
// ADR1
25
  PIO0->INSTR_MEM1 = 0x00c0;      // jmp FIN1_PIN -> ADR0 warten bis '0'
26
// input-pin 1 -> 0
27
  PIO0->INSTR_MEM2 = 0x4020;      // mov x->isr, x-Wert ausgeben
28
// ADR3
29
  PIO0->INSTR_MEM3 = 0x0044;      // jmp x-- -> ADR4
30
// ADR4  
31
  PIO0->INSTR_MEM4 = 0x00c1;      // jmp FIN1_PIN 0-> 1 dann ADR0
32
  PIO0->INSTR_MEM5 = 0x0103;      // jmp ADR3, 1 x delay
33
  
34
  PIO0_SET->SM0_SHIFTCTRL = PIO0_SM0_SHIFTCTRL_FJOIN_RX_Msk | // FIFO = 8
35
                            PIO0_SM0_SHIFTCTRL_AUTOPUSH_Msk;  // autopush
36
  PIO0_SET->CTRL = (1 << (PIO0_CTRL_SM_ENABLE_Pos));          // starten
37
}
38

39

40
// Zeitstempel erzeugen:
41
// DMA-Wert = Zeitpunkt, DMA-TRANS-COUNT = Ereignisse
42
// wenn CH0 fertig wird CH10 angestossen, der wiederum CH0 startet
43
// daher ist keine ISR notwendig
44
void init_dma_pio0(void)
45
{
46
  RESETS_SET->RESET = RESETS_RESET_dma_Msk;   // reset all CHx
47
  RESETS_CLR->RESET = RESETS_RESET_dma_Msk;
48
  while (!(RESETS->RESET_DONE & RESETS_RESET_dma_Msk));
49

50
// Zaehlen der Ereignisse und lesen des Zeitstempels  
51
  DMA->CH0_READ_ADDR = (uint32_t) &PIO0->RXF0;
52
  DMA->CH0_WRITE_ADDR = (uint32_t) &F1_time_stamp;
53
  DMA->CH0_TRANS_COUNT = DMA_COUNT;
54
  DMA->CH0_CTRL_TRIG =  DREQ_PIO0_RX0 << DMA_CH0_CTRL_TRIG_TREQ_SEL_Pos |     // PIO0-RX0
55
                        10 << DMA_CH0_CTRL_TRIG_CHAIN_TO_Pos |  // trigger CH10
56
                        2 << DMA_CH0_CTRL_TRIG_DATA_SIZE_Pos |  // word
57
                        DMA_CH0_CTRL_TRIG_EN_Msk;               // enable
58

59
// Hilfskanal zum Nachtriggern con CH0  
60
  DMA->CH10_READ_ADDR = (uint32_t) &dma_dummy;                  // dummy location
61
  DMA->CH10_WRITE_ADDR = (uint32_t) &dma_dummy;
62
  DMA->CH10_TRANS_COUNT = 1;                                    // single transfer
63
  DMA->CH10_CTRL_TRIG =  DREQ_AUTO << DMA_CH0_CTRL_TRIG_TREQ_SEL_Pos | // free running
64
                        0 << DMA_CH0_CTRL_TRIG_CHAIN_TO_Pos |   // Trigger CH0  
65
                        2 << DMA_CH0_CTRL_TRIG_DATA_SIZE_Pos |  // word
66
                        DMA_CH0_CTRL_TRIG_EN_Msk;               // enable
67
}
#7948325
Lesenswert?

Christoph M. schrieb:

Sind das Messungen? Wenn ja, dann sind sie verdächtig: 1 Bit Quantisierungsfehler sollte immer drinn sein.

Das sind Messungen. Aber auf einem Controller. PIO1 erzeugt die Frequenz auf GPIO16, PIO0 misst sie auf GPIO17. Da muss man nicht so eine große Brücke bauen! ;-)

Also 100% synchron. Aber du hast recht, wenn man etwas Externes messen würde, dann käme 1 cycle Unschärfe hinzu. (Plus die ppms der beteiligten Quarze)

: Bearbeitet durch User
#7951973
Lesenswert?

Christoph M. schrieb:

So wie es aussieht, entstehen hier Fehler bis zu ca. 20%.

von Mi N. (msx) 08.10.2025 09:15

Das wundert nicht, da ohne richtige Synchronisierung von Eingangssignal und Referenztakt keine stabile Messungen möglich sind. Entweder verwendest Du externe D-FFs oder eine interne PIO + DMA.

Da ist mir nicht ganz klar, wie du das meinst. Eigentlich sollte der Jitter ja nur 1 Taktzyklus betragen. Wie oben gezeigt, schafft das Norbert ja mit 5ns Auflösung. Wenn man nicht mit einem synchronisiert, sollte das dann +-5ns sein.

#7951989
Lesenswert?

Christoph M. schrieb:

Da ist mir nicht ganz klar, wie du das meinst. Eigentlich sollte der Jitter ja nur 1 Taktzyklus betragen.

Ja, aber erst, wenn man auch die Verarbeitung der Ereignisse dem 2. Kern überträgt. Mit "richtiger Synchronisierung" meine ich, daß man synchron zu einem Eingangsereignis dessen Zählerstand und auch des Referenzzählers einfriert ('capture') und später verarbeiten kann. Das war bei Deiner eingangs gezeigten Programmversion ja nicht der Fall. Loop() bei Arduino bedeutet, daß noch viele 'unsichtbare' Funktionen ausgeführt werden, die zeitlich nicht begrenzt sein müssen. Anders wäre es bei einer while()-Schleife, wo nur ISRs dazwischen funken können.

#7952205
Lesenswert?

Christoph M. schrieb:

Norbert schrieb

Alles bis ein paar hundert MHz und wenige Nanosekunden lässt sich damit ja prima handeln.

Ja, den Frequenzzähler von Mi N. hätte man wahrscheinlich auch gut in Micropython machen können, vermute ich.

Noch einfacher und ohne DMA. Das hier ist alles was man braucht:

1
from array import array
2
from rp2 import PIO, StateMachine, asm_pio
3
from machine import freq as cpufreq

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren