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.
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.
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?
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'.
'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
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
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:
Wieseo ist die Genauigkeit eingeschränkt?
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)
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.
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.
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.
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
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.
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).
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
structHcsr04{
7
uint32_tsum;
8
uint8_tnum,bad;
9
}hcsr04;
10
11
voidtimer1_setup(){
12
TCCR1A=0;
13
TCCR1B=(1<<CS11);// Prescaler 8, 2 ticks per μs at 16MHz.
14
TCCR1C=0;
15
}
16
17
voidhcsr04_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_tstick=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_tetick=ICR1;// Retrieve timer value stored at falling edge
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.
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.
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.