Hallo zusammen. Kurz etwas zu mir. Bin Rentner und möchte mich etwas in der AVR Programmierung in C weiterbilden. Beruflich hatte ich bisher noch nie etwas mit dem programmieren zu tun. Privat hat mich die Elektronik und das Programmieren schon vor über 40 Jahren gepackt. Damals kamen die TTL's aus der 74 Serie zu erschwinglichen Preisen auf den Markt. Im Selbststudium habe ich mich dann in die Elektronik und das programmieren eingearbeitet. Vom hgw. BASIC , VB3.0, BASCOM über ARDUINO komme ich nun zum C. Ich will endlich wissen, was intern wirklich passiert. Fertige Funktionen kann fast jeder verwenden, aber was dahinter steckt ist interessanter. Und um es vorweg zu nehmen: ASM passt nicht mehr in meinen alten Kopf. Das ist mir nun wirklich zu Kryptisch. Nun zu meiner eigentlichen Frage: Ich will mit dem Ultraschallsensor HC-SR04 rumspielen. Klar gibt es unzählige fertige Programme dafür. In BASCOM und beim ARDUINO funktioniert es relativ gut. Ich will aber selbst etwas machen. Damit verstehe ich es dann auch besser. Ich möchte den Timmer1 als Input Capture nutzen TCCR1A bleibt auf 0x00 TCCR1B ICES1 auf 0 (für fallende Flanke ) CS10 bis 12 Prescaller einstellen TIMSK1 ICIE1 für den Input Capture Interrupt setzen TOIE1 für Überlauf setzen Dann im Programm den Timer mit sei() starten Wenn am ICP1 PIN eine fallende Flanke erkannt wird => Sprung in ISR TIMER1 CAPT. in der ISR : -ICR1 in 16 Bit Variable speichern -Timer1 mit cli() stoppen ISR ENDE Auswerten der Daten Ist meine Logik so für den Programmablauf richtig?
Ohne jetzt nochmal selber im Datenblatt die Bedeutung der Bits nachzuschauen, kann ich schonmal sagen cli und sei beeinflussen nur die Freigabe aller Interrupts, der Timer läuft weiter, ohne dass eine Interruptroutine abgearbeitet werden müsste. Er läuft weiter nach einem Überlauf, er kopiert seinen Stand in das Input Capture Register, falls er so eingestellt ist, ohne dass die CPU ihm dabei helfen müsste. Man kann sogar bei gesperrten Interrupts den Wert aus dem Input Capture Register auslesen. Die CPU unterbricht halt nur nicht die Abarbeitung des Hauptprogramms für einen Interrupt. Zähler starten und stoppen geht über ein oder mehrere Bits, die sich meist im Control Register für den Timer befinden. Bei manchen Controllern stoppt man den Timer, indem man seinen Takt im Vorteiler abschaltet.
> Ist meine Logik so für den Programmablauf richtig?
Das Konzept ist wohl noch nicht ganz vollständig: gefragt ist ja die
Zeitspanne des 'Echo Pulse Output' zwischen steigender und fallender
Flanke, nachdem zuvor ein 'Trigger Input to Module' auf das HC-SR04
gegeben wurde.
Peter H. schrieb: > Dann im Programm den Timer mit sei() starten
1 | sei(); // Enable Global Interrupts |
https://github.com/SmithIsMyName/ATMega328p_HC-SR04_DMD/blob/master/main.c
Peter H. schrieb: > Ist meine Logik so für den Programmablauf richtig? Nein, da fehlt noch der Zeitpunkt der steigenden Flanke. Schließlich will man ja die Pulsdauer haben und nicht den Timestamp einer Flanke seit Programmstart. Typisch läßt man den Timer durchlaufen und bildet die Differenz aus den Timstamps beider Flanken.
Jetzt nochmal mit Datenblatt : Falls nicht nur Software von Dir läuft, schauen, dass nicht irgendein Öko Bit 3 im PRR gesetzt hat oder einfacher : es unbesehen löschen. Wenn Du nichts ausgeben willst, ist der Waveform Generation Mode 0 vermutlich der, den Du brauchst. Also WGM10, WGM11, WGM12 und WGM13 auf 0 setzen. Du willst keine Ausgangspins vom Timer ändern lassen ? Dann setze COM1A1, COM1A0, COM1B1 und COM1B0 auf 0. Ergibt zusammen den Wert 0 für TCCR1A. Den Wert für TCCR1B kannst Du Dir ja jetzt selber zusammensuchen. Ein paar Bits davon hatte ich ja schon erwähnt. Die anderen musst Du setzen, je nachdem, ob Du verhindern magst, dass extrem kurze Pulse ein ICP auslösen, welche Flanke ICP auslösen soll und wie schnell der Timer zählen soll. Wobei Takt vom T1 Pin beziehen höchstwahrscheinlich nicht das ist, was Du willst. Und schon müsste der Zähler zählen... also die Werte in TCNT1 sollten sich ändern. Ich würde sagen, schieb das mit den Interrupts auf später, lass also TIMSK1 auf 0 stehen. Schreibe zum Schluss der Initialisierung 0x27U ins TIFR1. Wenn Bit 5 vom TIFR1 gesetzt ist, hat mindestens ein ICP stattgefunden. Schreibe dann 0x20U in TIFR1, um das Bit zu löschen (klingt erstmal komisch, ist aber so und hat durchaus seinen Grund). Dann kannst Du ICR1 auslesen, um zu erfahren bei welchem Zählerstand das letzte ICP stattgefunden hat. Wenn Bit 0 vom TIFR1 gesetzt ist, ist der Timer übergelaufen. Dieses Bit löscht Du durch schreiben von 0x01U ins TIFR1.
Flunder schrieb: > Dann kannst Du ICR1 > auslesen, um zu erfahren bei welchem Zählerstand das letzte ICP > stattgefunden hat. Zuerst mal vielen Dank für die vielen Antworten. Noch eine kurze Frage zu ICR1: Kann ich ICR1 direkt in eine 16 Bit Variable speichern, oder muss ich erst ICR1H und dann ICR1L lesen?
> Kann ich ...
So etwas übernimmt der C-Compiler, also Ersteres.
> dann hat man etwas Schreibarbeit gespart
Zwar wurde ja gleich zu Beginn geklärt, dass solche Überlegungen zu
"kryptisch" sind, trotzdem sei darauf hingewiesen, dass es sich nicht um
reine Bequemlichkeit handelt: 'For a 16-bit read, the low byte must be
read before the high byte'.
S. L. schrieb: > 'For a 16-bit read, the low byte must be > read before the high byte'. Jetzt bin ich durcheinander. Kann ich jetzt uint16_t wert=ICR1; schreiben oder das LowByte zuerst und dann erst das HighByte? oder macht das der Compiler ohne mein zutun
Peter H. schrieb: > Kann ich jetzt > uint16_t wert=ICR1; > schreiben Ja. Der Compiler muss schon alles richtig umsetzen.
:
Bearbeitet durch User
Peter H. schrieb: > oder macht das der Compiler ohne mein zutun Das macht der Compiler für dich, der kennt diese Eigneart bei den 16-bit Registern. Also einfach uint16_t Werte benutzen.
:
Bearbeitet durch User
> wert=ICR1; Dies - einfach ausprobieren (der Compiler macht das richtig). Ihre ursprüngliche Alternative > erst ICR1H und dann ICR1L lesen? war aber falsch - nur darauf wollte ich hinweisen.
S. L. schrieb: > Ihre ursprüngliche Alternative >> erst ICR1H und dann ICR1L lesen? > war aber falsch - nur darauf wollte ich hinweisen. Stimmt, habe schreiben mit lesen verwechselt. Beim schreiben gilt diese Reihenfolge
Peter H. schrieb: > im Programm den Timer mit sei() starte > in der ISR -Timer1 mit cli() stoppen Nein, so macht man das nicht. Mit sei()/cli() kann man den Timer nicht beeinflussen. Damit kann man nur bestimmen, ob die CPU irgendwelche (Timer und alle anderen) Interrupt-Routinen anspringt oder nicht. Durch Schreiben in TIMSK1 kann man bestimmen, ob der Timer Interrupts auslöst oder nicht, er läuft aber unabhängig von dieser Einstellung so, wie du ihn initialisiert hast. Am besten versuchst du es zunächst mal ganz ohne Interrupts, du kannst ja in einer Schleife TIFR1 auslesen, um zu sehen, wann das Capture-Event kommt.
Ich habe mal etwas mit dem HC-SR04 nach euren Angaben rumgespielt. Die Idee mit dem PIN Change Interrupt war gut. Da kann man ja ohne Probleme mehrere Ultraschall Sensoren einsetzen. Aber ich bin ja immer noch neugierig. Mir geistert immer noch der TIMER1 CAPT Interrupt im Kopf rum. So stelle ich mir das vor: Ich schicke einen 10us Impuls auf den Trigger des HC. gleichzeitig stelle ich den Timer1 mit TCNT1=0 zurück. Wenn ich den Echo PIN mit ICP1 Pin verbinde (mit fallender Flanke) entsteht doch am Ende des ECHO Signals ein TIMER1 CAPT Interrupt. Im Interrupt lese ich ICR1 aus Das Ergebnis ist die Anzahl der Ticks vom Timer1. ( muß nicht mehr berechnet werden und kann direkt weiterverarbeitet werden) Ist das OK so, oder vielleicht nur eine Spinnerei von mir ( nach dem Motto: Geht nicht, gibt es nicht oder Versuch macht kluch )
Ja, geht; die Genauigkeit ist etwas eingeschränkt. Ansonsten: zuerst ICP auf positive Flanke, in der ISR (Start des Echo-Signals) ICR lesen und speichern und ICP auf negative Flanke umstellen, im anschließenden Interrupt (das Ende des Echo-Signals) wieder ICR lesen und von diesem Wert den vorher gespeicherten abziehen. PS: Das geht auch (wie mehrfach vorgeschlagen) ohne Interrupt, also nur durch Abfrage (und explizite Löschung) von TIFR1.ICF1, aber viel einfacher wird es dadurch nicht. PPS: Fällt mir jetzt erst auf: > ... nach euren Angaben rumgespielt. Die Idee mit dem > PIN Change Interrupt war gut Finde ich nicht, wo steht das?
:
Bearbeitet durch User
S. L. schrieb: > Ja, geht; die Genauigkeit ist etwas eingeschränkt. > > Ansonsten: zuerst ICP auf positive Flanke, in der ISR (Start des > Echo-Signals) ICR lesen und speichern und ICP auf negative Flanke > umstellen, im anschließenden Interrupt (das Ende des Echo-Signals) > wieder ICR lesen und von diesem Wert den vorher gespeicherten abziehen. 2 Fragen dazu: 1. Wieseo ist die Genauigkeit eingeschränkt? 2. Wenn ich direkt ICP auf fallende Flake einstelle und direkt mit dem Start des Triggersignals Timer1 mit TCNT1=0 zurücksetze z.B. PB1=1; TCNT1=0; _delay_us(10); PB1=0; dürfte ich doch nur sehr wenige Systemtakte verlieren. (Nur die Dauer für das Rücksetzen von TCNT1)
> Wieseo ist die Genauigkeit eingeschränkt? Deshalb: > wenige Systemtakte verlieren Wieviele das sind, würde mich selbst interessieren, probieren Sie beide Methoden aus und berichten Sie.
Peter H. schrieb: > ein TIMER1 CAPT Interrupt Denke dran, in dieser ISR den Interrupt selber zu sperren, weil nur das erste Echo das ist, was du messen möchtest. Erst für den nächsten Messzyklus den CAPT IRQ wieder freigeben.
:
Bearbeitet durch User
> dürfte ich doch nur sehr wenige Systemtakte verlieren
Oh nein, das Triggersignal ist keineswegs der Beginn des 'Echo Pulse
Output', dazwischen liegt der '8 Cycle Sonic Burst' und dessen
Auswertung, und damit eine Zeitspanne, über die das Datenblatt nichts
aussagt.
PS:
Zeitdiagramm angehängt
:
Bearbeitet durch User
Matthias S. schrieb: > Denke dran, in dieser ISR den Interrupt selber zu sperren, weil nur das > erste Echo das ist, was du messen möchtest. Erst für den nächsten > Messzyklus den CAPT IRQ wieder freigeben. Das brauche ich nicht unbedingt, da ich die Messzyklen erst wieder nach der kompletten Abarbeitung der Auswertung der Ergebnisse selbst im Programm wieder starte. Die automatisierten Messzyklen in den Programmen im Netz haben mich immer gestört. Die haben mir immer in meine Auswertung gepfuscht und die Ergebnisse gestört. Trotzdem schalte ich nach der ISR den ICIE1 wieder aus (falls Störimpule auf der Echoleitung auftreten) und kurz vor dem Triggerimpuls wieder ein. Danke für den Tip!
S. L. schrieb: > Oh nein, das Triggersignal ist keineswegs der Beginn des 'Echo Pulse > Output', dazwischen liegt der '8 Cycle Sonic Burst' und dessen > Auswertung, und damit eine Zeitspanne, über die das Datenblatt nichts > aussagt. Oh, vielen Dank für die Hilfe. Kann ich einige Referenzmessungen durchführen und den Mittelwert des Fehlers vom Ergebnis abziehen? Dadurch könnte ich dann die Impulse zwischen Timerstart und Start des Echo Signals ermitteln Oder macht es doch mehr Sinn, den Echo PIN mit Pinchange Interrupt zu überwachen. D.h. Start Timer ( 1. Interrupt) bei steigender Flanke Stop Timer (2. Interrupt) bei fallender Flanke
:
Bearbeitet durch User
Ich kann Ihrem Gedankengang nicht ganz folgen (bin auch zu müde). Nur soviel, gerade ausprobiert: bei meinem Exemplar liegt zwischen pos. Flanke des Triggersignals und pos. Flanke des Echos eine Zeitspanne von rund 2.2 ms, unabhängig von der Objektentfernung. PS: also tatsächlich 2,2 Millisekunden
:
Bearbeitet durch User
Peter H. schrieb: > ... über ARDUINO komme ich nun zum C. Ich will endlich > wissen, was intern wirklich passiert. Übrigens, wenn man auf Arduino verzichtet, kann man dann auf die obsoleten AVRs auch verzichten.
Georg M. schrieb: > Übrigens, wenn man auf Arduino verzichtet .... geht der Spass erst richtig los. Wer kann der kann ....
Wie das geht seht exact beschrieben in der Microchip AppNote "AVR135: Using Timer Capture to Measure PWM Duty Cycle". Nichts anderes ist das. Ein alter Hut. Neuere Chips der NULL-Serie haben dafür eine HW Funktion (Pulse Width detection). https://www.microchip.com/content/dam/mchp/documents/OTH/ApplicationNotes/ApplicationNotes/Atmel-8014-Using-Timer-Capture-to-Measure-PWM-Duty-Cycle_ApplicationNote_AVR135.pdf
S. L. schrieb: > Oh nein, das Triggersignal ist keineswegs der Beginn des 'Echo Pulse > Output', dazwischen liegt der '8 Cycle Sonic Burst' und dessen > Auswertung, und damit eine Zeitspanne, über die das Datenblatt nichts > aussagt. > > PS: > Zeitdiagramm angehängt Der Text in diesem Zeitdiagramm ist sehr sehr mißverständlich. Der HC-SR04 funktioniert so: Man erzeugt einen Trigger-Impuls von ~10us Dauer. Dies veranlasst den HC-SR04 seine 8 Ultraschallwellen auszustoßen, und danach Echo auf HIGH zu setzen. Nach einer Weile läuft das Ultraschallecho beim HC-SR04 ein und nach der Erkennung der 8 Wellen setzt der HC-SR04 Echo wieder auf LOW. Die Laufzeit des Ultraschallsignals entspricht also der Zeit zwischen Echo LOW->HIGH und Echo HIGH->LOW. Die genaueste Art und Weise diese Laufzeiten mit einem Atmega328P zu ermitteln ist über ICP. Interrupts sind dafür oft nicht nötig, wenn man die Wartezeit bis zum Abbruchkriterium akzeptieren kann. Mein Code dazu sieht in etwa so aus:
1 | #define DIST_DDR DDRD
|
2 | #define DIST_PORT PORTD
|
3 | #define DIST_TRIGGER (1<<PIND6) // TRIGGER
|
4 | #define DIST_ECHO (1<<PIND6) // PD6 is used as ICP1. The ECHO signal is connected to PD6 via 1k (TRIGGER is connected directly).
|
5 | |
6 | struct Hcsr04 { |
7 | uint32_t sum; |
8 | uint8_t num, bad; |
9 | } hcsr04; |
10 | |
11 | void timer1_setup () { |
12 | TCCR1A = 0; |
13 | TCCR1B = (1<<CS11); // Prescaler 8, 2 ticks per μs at 16MHz. |
14 | TCCR1C = 0; |
15 | }
|
16 | |
17 | void hcsr04_measure () { |
18 | DIST_DDR |= DIST_TRIGGER; // Make DIST_TRIGGER OUTPUT |
19 | DIST_PORT &= ~DIST_TRIGGER; // OUTPUT LOW |
20 | delayMicroseconds(10); // Output LOW explicitely. |
21 | DIST_PORT |= DIST_TRIGGER; // OUTPUT HIGH |
22 | delayMicroseconds(10); // Wait long enough for the sensor to realize the trigger pin is high. Sensor specs say to wait 10μs. |
23 | DIST_PORT &= ~DIST_TRIGGER; // OUTPUT LOW |
24 | delayMicroseconds(10); // Output LOW explicitely. |
25 | DIST_DDR &= ~DIST_ECHO; // Switch to INPUT |
26 | delayMicroseconds(10); // Give echo some time to go low |
27 | TCNT1 = 0; |
28 | TCCR1B |= (1<<ICES1); // Trigger on rising edge of ICP1. |
29 | TIFR1 |= (1<<ICF1) | (1<<TOV1); // Clear flags by writing 1 |
30 | while ((TIFR1 & ((1<<ICF1)|(1<<TOV1))) == 0) continue; // Wait for rising edge or overflow (after 32ms) |
31 | if (TIFR1 & (1<<ICF1)) { |
32 | uint16_t stick = ICR1; // Retrieve timer value stored at rising edge |
33 | TCCR1B &= ~(1<<ICES1); // Trigger on falling edge of ICP1 |
34 | TIFR1 |= (1<<ICF1); // Clear flag by writing 1 |
35 | while ((TIFR1 & ((1<<ICF1)|(1<<TOV1))) == 0) continue; // Wait for falling edge or overflow (after 32ms) |
36 | if (TIFR1 & (1<<ICF1)) { |
37 | uint16_t etick = ICR1; // Retrieve timer value stored at falling edge |
38 | hcsr04.sum += etick - stick; |
39 | hcsr04.num++; |
40 | } else { |
41 | hcsr04.bad++; |
42 | }
|
43 | } else { |
44 | hcsr04.bad++; |
45 | }
|
46 | }
|
LG, Sebastian
:
Bearbeitet durch User
Sebastian W. schrieb: > Der Text in diesem Zeitdiagramm ist sehr sehr mißverständlich. Für mich war der Text sogar nahezu unverständlich, dafür schien mir das Diagramm selbst eindeutig: S. L. schrieb: > gefragt ist ja die > Zeitspanne des 'Echo Pulse Output' zwischen steigender und fallender > Flanke
Nach unzähligen Fehlversuchen habe ich mal meinen Ultraschallsensor an das Oszi gehangen. Siehe da, es sendet kein Echo. Also ein funktionierenden genommen und siehe da es kommt zumindest mal ein Ergebnis raus. Hier ist meine Version für den Ultraschallsensor. Verbesserungsvorschläge nehme ich gerne entgegen. Ich muss sie nur mit meinem begrenzten Wissen verstehen können. void HC_SR04_start () { TIMSK1 |=(1<<ICIE1); // Interrupt Starten PORTD |=(1<<PORTD0); // Trigger einschalten _delay_us(10); // Trigger dauer PORTD &=~(1<<PORTD0); // Trigger ausschalten } void Timer1_init () { TCCR1A = 0x00; // nicht gebraucht TCCR1B |=(1<<CS11); // Prescaller von 8 TCCR1B |=(1<<ICES1); // steigende Flanke TCCR1C = 0x00; // nicht gebraucht TIMSK1 |=(1<<ICIE1); // Interrupt Starten } ISR (TIMER1_CAPT_vect) { if (start==1) { TCNT1=0; // Timer zurücksetzen TCCR1B &=~(1<<ICES1); // umstellen aud fallende Flanke start=0; // für Interrupt } else { TIMSK1 &=~(1<<ICIE1); // Interrupt Stoppen Dezimalwert=ICR1; // Timerstand übergeben TCCR1B |= (1<<ICES1); // umstellen auf steigende Flanke start=1; // für Interrupt } } In der Main rufe ich nur sporadich die HR... Funktion auf und lasse dann die Variable Dezimalwert auf meiner LCD Funktion ausgeben.
Peter H. schrieb: > Verbesserungsvorschläge nehme ich gerne entgegen. Ich muss sie nur mit > meinem begrenzten Wissen verstehen können. Für den Anfang ganz OK. > void Timer1_init () > { > TCCR1A = 0x00; // nicht gebraucht > TCCR1B |=(1<<CS11); // Prescaller von 8 > TCCR1B |=(1<<ICES1); // steigende Flanke > TCCR1C = 0x00; // nicht gebraucht > TIMSK1 |=(1<<ICIE1); // Interrupt Starten > } > ISR (TIMER1_CAPT_vect) > { > if (start==1) > { > TCNT1=0; // Timer zurücksetzen > TCCR1B &=~(1<<ICES1); // umstellen aud fallende Flanke > start=0; // für Interrupt > } Bei Input Capture muss man den Timer nicht anhalten, man läßt ihn einfach laufen. Die Differenz aus STOP-START Wert ist immer korrekt, auch beim Überlauf, solange die Differenz kleiner als der halbe, maximale Zählerstand ist, hier bei Timer 1 816 Bit) 32768. > else > { > TIMSK1 &=~(1<<ICIE1); // Interrupt Stoppen > Dezimalwert=ICR1; // Timerstand übergeben > TCCR1B |= (1<<ICES1); // umstellen auf steigende Flanke > start=1; // für Interrupt > } > } > In der Main rufe ich nur sporadich die HR... Funktion auf und lasse dann > die Variable Dezimalwert auf meiner LCD Funktion ausgeben. Hoffentlich gesichert über volatile und mit atomarem Zugriff, siehe Interrupt. Beitrag "Re: Atmega/Arduino - Input Capture funktioniert nicht"
Peter H. schrieb: > Auswerten der Daten Man kann mit den rein analogen Varianten des HC-SR04 übrigens eine wesentlich höhere Genauigkeit als 3mm erreichen. Anbei der Verlauf des Wasserstandes meines Außenteichs als minütlichem simplem Mittelwert von 60 Messungen. LG, Sebastian
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.