Hallo,
ich möchte seriell die Distanz eines HC-SR04 Sensors ausgeben. Die
Ausgabe funktioniert super. Außerdem habe ich auch eine Funktion die wie
micros() von Arduino funktioniert. Aber statt eine Funktion ist es eine
Variable.
Ich mache alles in Atmel Studio auf einem Arduino Mega 2560, benutze
aber nicht die Arduino IDE und ihren Code. Manchen Code habe ich aus der
NewPing libary kopiert und verändert.
Leider gibt mir meine Funktion aber nur falsche Werte aus. Wenn ich die
Hand ca. 10cm vor den Sensor halte, bekomme ich den Wert 23-24. Wenn er
kein Signal zurück bekommt gibt er 0 aus. Ich würde den richtigen
CM-Wert erwarten.
Könnt ihr mir bitte meine Fehler sagen? Hier ist der Code.
main.c
1
2
#include<avr/io.h>
3
#include"Serial.h"
4
#include"Ultrasonic.h"
5
#include<util/delay.h>
6
#include"avr/interrupt.h"
7
8
intmain(void){
9
SerialInit();
10
UltrasonicInt();
11
12
// set up timer 0
13
TCCR0B=(1<<CS01);// Prescaler 8
14
TIMSK0|=(1<<TOIE0);
15
sei();
16
17
while(1){
18
SerialPrintNumberULong(UltrasonicRead(300,0));
19
SerialPrint("\n");
20
_delay_ms(100);
21
}
22
}
Ultrasonic.c
1
#include"Ultrasonic.h"
2
#include<util/delay.h>
3
#include<avr/interrupt.h>
4
5
unsignedlongmicroseconds=0;
6
7
#define US_ROUNDTRIP_CM 57 // Microseconds (uS) it takes sound to travel round-trip 1cm (2cm total), uses integer to save compiled code space
8
#define NO_ECHO 0 // Value returned if there's no ping echo within the specified MAX_SENSOR_DISTANCE or maxCmDistance
9
#define MAX_SENSOR_DELAY 5800 // Maximum uS it takes for sensor to start the ping
10
#define PING_OVERHEAD 5 // Ping overhead in microseconds (uS)
11
12
unsignedintmaxEchoTime;
13
unsignedlongmaxTime;
14
15
boolping_trigger(){
16
PINL&=~(1<<PL6);// Set the trigger pin low, should already be low, but this will make sure it is.
17
_delay_ms(4);// Wait for pin to go low.
18
PINL|=(1<<PL6);// Set trigger pin high, this tells the sensor to send out a ping.
19
_delay_ms(15);// Wait long enough for the sensor to realize the trigger pin is high. Sensor specs say to wait 10uS.
maxTime=microseconds+maxEchoTime+MAX_SENSOR_DELAY;// Maximum time we'll wait for ping to start (most sensors are <450uS, the SRF06 can take up to 34,300uS!)
26
while(PIND&(1<<PD0))// Wait for ping to start.
27
if(microseconds>maxTime)
28
returnfalse;// Took too long to start, abort.
29
maxTime=microseconds+maxEchoTime;// Ping started, set the time-out.
30
returntrue;// Ping started successfully.
31
}
32
33
voidUltrasonicInt(void){
34
DDRL|=(1<<PL6);//Trigger as OUTPUT
35
DDRD&=~(1<<PD0);//Echo as INPUT
36
}
37
38
unsignedlongUltrasonicRead(longmaxCM,uint8_tnr){
39
switch(nr){
40
case0:
41
if(maxCM>0)
42
maxEchoTime=(maxCM+1)*US_ROUNDTRIP_CM;// Call function to set a new max sensor distance.
43
44
if(!ping_trigger())
45
returnNO_ECHO;// Trigger a ping, if it returns false, return NO_ECHO to the calling function.
46
47
while(PIND&(1<<PD0))// Wait for the ping echo (Read echo)
48
if(microseconds>maxTime)
49
returnNO_ECHO;// Stop the loop and return NO_ECHO (false) if we're beyond the set maximum distance.
50
51
unsignedlongechoTime=(microseconds-(maxTime-maxEchoTime)-PING_OVERHEAD);// Calculate ping time, include overhead.
52
if(echoTime)
53
return((unsignedint)echoTime+US_ROUNDTRIP_CM/2);//(max(((unsigned int) echoTime + US_ROUNDTRIP_CM / 2) / US_ROUNDTRIP_CM, 1)); // Convert uS to centimeters.
54
else
55
return555;//(max(((unsigned int) echoTime + US_ROUNDTRIP_CM / 2) / US_ROUNDTRIP_CM, 0)); // Convert uS to centimeters
Dein Mikrosekunden-Timer ist viel zu schnell. So schnell kann der AVR
die ISR gar nicht ausführen. Außerdem muss die Variable als volatile
gekennzeichnet werden.
Ich schlage einen anderen wesentlich simpleren Lösungsansatz ohne Timer
und ohne diese umfangreiche Mathematik vor:
http://stefanfrings.de/hc-sr04/index.html
Stefanus F. schrieb:> Dein Mikrosekunden-Timer ist viel zu schnell. So schnell kann der AVR> die ISR gar nicht ausführen. Außerdem muss die Variable als volatile> gekennzeichnet werden.>> Ich schlage einen anderen wesentlich simpleren Lösungsansatz ohne Timer> und ohne diese umfangreiche Mathematik vor:> http://stefanfrings.de/hc-sr04/index.html
Vielen Dank. Es funktioniert. Aber das Problem ist, dass ich ca. 10
Sensoren benutzen möchte und das spätere Programm in 10-20ms Takt
arbeiten soll. Alle 3-6 Takte soll er dann die Sensoren abfragen. Dafür
ist die Methode dann nicht die beste. Kannst du mir sagen wie ich bei
meinem Code den Timer langsamer stellen kann?
Fabio M. schrieb:> Kannst du mir sagen wie ich bei> meinem Code den Timer langsamer stellen kann?
Das ergäbe wenig Sinn. 10µs wären vielleicht machbar, aber dann wird die
Messung schon recht ungenau. Das problem ist auch, dass du mit zu vielen
Interrupts riskierst, dass dein Hauptprogramm zu langsam wird und daher
das Ergebnis ungenau wird.
Ich nehme mal an, dass die 10 Sensoren räumlich getrennt sind und
gleichzeitig arbeiten können. Dann würde ich so vorgehen:
Triggere alle 10 Sensoren gleichzeitig, lass dann die 58µs Schleife so
oft wiederholen, bis alle Sensoren das Echo erfasst haben (oder in den
Timeout gelaufen sind). Du kannst für jeden Sensor einzeln die Zeit
speichern. Etwa so:
1
2
int16_tcm=0;
3
4
// Trigger a measurement
5
TRIGGER_ON;
6
_delay_us(10);
7
TRIGGER_OFF;
8
9
// Wait for response
10
// Assuming that all sensors need the same time
11
while(!ECHO_SIGNAL_SENSOR1);
12
13
// Measure pulse width of all 10 sensors
14
while(cm<300)
15
{
16
if(ECHO_SIGNAL_SENSOR1)value1=cm;
17
if(ECHO_SIGNAL_SENSOR2)value2=cm;
18
if(ECHO_SIGNAL_SENSOR3)value3=cm;
19
if(ECHO_SIGNAL_SENSOR4)value4=cm;
20
if(ECHO_SIGNAL_SENSOR5)value5=cm;
21
if(ECHO_SIGNAL_SENSOR6)value6=cm;
22
if(ECHO_SIGNAL_SENSOR7)value7=cm;
23
if(ECHO_SIGNAL_SENSOR8)value8=cm;
24
if(ECHO_SIGNAL_SENSOR9)value9=cm;
25
if(ECHO_SIGNAL_SENSOR10)value10=cm;
26
_delay_us(57);
27
cm++;
28
}
Die 58µs habe ich auf 57 reduziert, um die Zeit auszugleichen, die für
die ganzen if-Abfragen drauf geht. Vielleicht muss man sogar noch etwas
mehr abziehen. Das kannst du ja mal ausmessen oder anhand des
Assembler-Listings ausrechnen.
Das Prinzip müsste so klar sein, oder?
Joachim B. schrieb:> dann schmeisse die delay raus und baue das zu einer state machine um
Ja, das wäre auch denkbar, dann braucht es allerdings einen Timer der im
58µs Raster hoch zählt.
Schwierig stelle ich mir dann allerdings vor, den richtigen Moment für
den Beginn der Messung abzupassen. Ich meine den Moment, wo das Echo
Signal auf High geht.
Ich bemerke grade einen Fehler: bei den 10 if Abfragen muss ein ! rein,
weil das Echo Signal der Sensoren auf Low geht, wenn die Messung beendet
ist.
Stefanus F. schrieb:> Joachim B. schrieb:>> dann schmeisse die delay raus und baue das zu einer state machine um>> Ja, das wäre auch denkbar, dann braucht es allerdings einen Timer der im> 58µs Raster hoch zählt.>> Schwierig stelle ich mir dann allerdings vor, den richtigen Moment für> den Beginn der Messung abzupassen. Ich meine den Moment, wo das Echo> Signal auf High geht.>> Ich bemerke grade einen Fehler: bei den 10 if Abfragen muss ein ! rein,> weil das Echo Signal der Sensoren auf Low geht, wenn die Messung beendet> ist.
Welchen Timer bzw. Prescaler sollte ich dafür wählen? Kann mir jemand
auf die Sprünge helfen?
Fabio M. schrieb:> Welchen Timer bzw. Prescaler sollte ich dafür wählen? Kann mir jemand> auf die Sprünge helfen?
Probiers aus, denk selber nach.
Welchen statistischen Fehler hat die Länge deines Eingangssignals?
Das ist der Maßstabe für deine Auflösung beim Timer. Die Zeit viel
genauer zu messen ist oft wenig sinnvoll, weil die letzten Bits dann nur
noch Hausnummern enthalten.
Wie lang ist deine längste Pulsdauer vom US-Geber?
Das muss dein Timer noch auszählen können.
u.s.w.
Fabio M. schrieb:> Welchen Timer bzw. Prescaler sollte ich dafür wählen? Kann mir jemand> auf die Sprünge helfen?
Ich denke jetzt nur mal laut, ohne ins Datenblatt zu schauen:
8MHz Systemtakt geteilt durch Prescaler 8 ergibt einen Timer-Takt von
1MHz (1µs). Wenn man den Timer jetzt so einstellt, dass er immer von
Null bis 58 zählt und dann einen Interrupt auslöst, wird die ISR alle
58µs aufgerufen.
58µs Intervalle ergeben Messwerte mit 1cm Auflösung. 29µs Intervalle
ergeben Messwerte mit 0,5cm Auflösung. Noch höhere Auflösung macht
keinen Sinn, weil die Messung alleine schon aufgrund von
Temperaturschwankungen viel mehr vom Soll abweicht.
> Wie lang ist deine längste Pulsdauer vom US-Geber?
Je nach Händler werden da 29ms (5 Meter) bis 41ms (7 Meter) angegeben.
Ich weiß aber aus Erfahrung, dass diese Sensoren nur bis etwa 17ms (3
Meter) zuverlässig funktionieren. Diesen Wert hatte der TO in seinem
ersten Programmentwurf auch als Limit festgelegt.
Da fällt mir gerade etwas auf: Fabio, du willtest im 10-20ms Intervall
messen. Aber so schnell sind die Sensoren gar nicht. Zwischen den
Messungen soll man mindestens 60ms warten.
Reicht für diese Anwendung ein 8bit Timer oder muss ich ein 16bit Timer
nehmen. Mein Sensor kann bis max ca. 3Meter messen.
Stefanus F. schrieb:> Da fällt mir gerade etwas auf: Fabio, du willtest im 10-20ms Intervall> messen. Aber so schnell sind die Sensoren gar nicht. Zwischen den> Messungen soll man mindestens 60ms warten.
Deshalb soll er nur jeden 6. Takt auslesen bzw. immer wenn er bereit
ist.
Fabio M. schrieb:> Reicht für diese Anwendung ein 8bit Timer oder muss ich ein 16bit Timer> nehmen. Mein Sensor kann bis max ca. 3Meter messen.
Wie viele Bits braucht man, um bis 58 zu zählen? Du schaffst das!
Ok, ich habe mal angefangen es umzusetzen. Ich habe den Timer0 (8Bit)
benutzt. Dieser zählt von 0-58, und das immer. Aber was mir noch nicht
klar ist, wann ich trigger bzw. lesen muss.
Stefanus F. schrieb:> Noch höhere Auflösung macht keinen Sinn, weil die Messung alleine> schon aufgrund von Temperaturschwankungen viel mehr vom Soll abweicht.
Das ist kein Argument. Der ATmega kann mit einem Quarz laufen und die
Abhängigkeit der Schallgeschwindigkeit von der Temperatur ist bekannt,
kann also ohne weiteres berücksichtigt werden, wenn man sich noch einen
Temperatursensor spendiert.
Da kommt es auf die Anforderungen an.
Solche Genauigkeit brauche ich nicht. Ein Zentimeter genau ist schon
mehr als nur perfekt.
Ich habe jetzt mal ein bisschen rumprobiert. Leider klappt es noch nicht
so. Er gibt mir immer 0 aus. Und nach ca. 10 Sekunden ist irgendetwas so
schief gelaufen, dass er nur noch nicht erkannte Zeichen ausgibt. Den
Fehler konnte ich selbst leider nicht finden. Hier ist der Code:
1
#include<avr/io.h>
2
#include"Serial.h"
3
#include<util/delay.h>
4
#include"avr/interrupt.h"
5
#include<stdbool.h>
6
7
volatileuint8_tusZumNachtsenCM=0;
8
volatileboolTrigger=true;
9
10
// The Ultrasonic sensor is connected to Port D2 and D3
Wenn dies das ganze Programm ist, wird die ISR niemals aufgerufen. Der
Timer wird gar nicht gestartet. Außerdem hast du munter Mikrosekunden
und 58µs Intervalle durcheinander gewürfelt.
Ich schlage vor, dass Du mal im Datenblatt das Kapitel für den Timer 0
mindestens 3 mal durchliest. Ziel ist, dass der Timer alle 58µs einen
Interrupt auslöst.
Dann lies meine Einleitung zu Zustandsautomaten:
http://stefanfrings.de/net_io/protosockets.html (die obere Hälfte). In
meinem Buch
http://stefanfrings.de/mikrocontroller_buch/Einstieg%20in%20die%20Elektronik%20mit%20Mikrocontrollern%20-%20Band%203.pdf
Band 3 Kapitel 9.1.4 findest du ein konkretes Anwendungsbeispiel mit
einer LED Anzeige.
Das Starten der Messung und das Messen der Pulsbreite solltest du
außerhalb der ISR machen, sonst läufst du ganz schnell Gefahr, dass
deine ISR selbst länger als 58µs benötigt und du dann wieder das gleiche
Problem bekommst, wie in deinem ersten Beitrag. Vergiss nicht, dass du
am Ende 10 Sensoren abfragen willst.
Alle Variablen, die in ISR geändert und außerhalb der ISR gelesen werden
(oder umgekehrt) müssen volatile sein. Außerdem muss man außerhalb der
ISR beim Zugriff auf Variablen größer als 8bit vorher cli() und danach
sei() aufrufen, damit die Variable nicht während des Zugriffes durch die
ISR verändert wird. Der Zugriff auf diese Variablem ist relativ
ineffizient, deswegen sollte man nicht unnötig viele verwenden.
Die Vorgehensweise ist so:
Die ISR soll alle 58µs vom Timer aufgerufen werden und einfach nur eine
Variable hochzählen. Nennen wir sie uint32_t systick (8bit wäre zu
klein).
1
uint32_tvolatilesystick=0;
2
3
ISR(TIMER1_OVF_vect)
4
{
5
systick++;
6
}
Dann schreibst du dir noch eine Funktion, mit der du systick sicher
lesen kannst ohne dass die ISR dazwischen funkt:
1
uint32_tgetSystick()
2
{
3
uint32_ttemp;
4
cli();
5
temp=systick;
6
sei();
7
returntemp;
8
}
;
In der Hauptschleife sendest du das Trigger Signal an den Sensor und
kopierst die Variable "systick" in eine Variable "started" die unbedingt
den gleichen Typ (uint32_t) haben muss, wie systick.
1
uint32_tstarted;
2
3
TRIGGER_ON;
4
_delay_us(10);
5
TRIGGER_OFF;
6
started=getSystick();
Dann wartest du, bis die Messung beendet ist.
1
if(!ECHO_SIGNAL)
2
{
3
cm=getSystick()-started;
4
}
Selbst wenn systick zwischendurch einmal überläuft (von 0xFFFF auf 0)
kommt dabei trotzdem das richtige Ergebnis heraus.
Jetzt hast du geschrieben, dass dein Hauptprogramm irgend etwas im 10ms
Intervall machen soll. Du musst also abfragen können, ob inzwischen 10ms
verstrichen sind. Das macht man so:
1
uint32_tanfang=getSystick();
2
3
...irgendwasmachen
4
5
uint32_tjetzt=getSystick();
6
if(jetzt-anfang>10000/58)
7
{
8
// es sind 10ms verstrichen
9
}
Um blöden Sprüchen von den erfahrenen Entwicklern zuvor zu kommen:
a) Anstatt cli() und sei() kann man auch die Makros aus der <atomic.h>
verwenden.
https://www.nongnu.org/avr-libc/user-manual/group__util__atomic.html
Manche Leute denken, dass der Code auf diese Weite besser auf andere
Mikrocontroller portierbar ist.
b) In Zustandsautomaten empfiehlt es sich, Enumeration anstatt Zahlen
für den Zustand zu benutzen. Denn dann hat man sprechende Namen und man
kann leichter etwas mittendrin einfügen.
Wolfgang schrieb:> Da kommt es auf die Anforderungen an.
Das habe ich schon berücksichtigt. Wer einen HC-SR04 verwendet hat keine
hohen Anforderungen. Wenn der halbwegs korrekte Zentimeter-Werte
liefert, kann man sich schon glücklich schätzen.
Wer mehr Genauigkeit braucht als +/-1 cm braucht, muss einen anderen
Sensor verwenden. Das wird dem Fabio angesichts des Preises des Sensors
sicher klar sein.
Fabio M. schrieb:> Solche Genauigkeit brauche ich nicht. Ein Zentimeter genau ist schon> mehr als nur perfekt.
Bei einer Temperaturänderung von 20 K weicht der Messungwert schon bei
einer Strecke von 30 Zentimeter um mehr als einen Zentimeter vom wahren
Wert ab.
Ich muss euch allen schon einmal vielmals Danken. Es funktioniert jetzt.
Ich habe den 10ms Takt erstmal weggelassen und nur 1 Sensor benutzt,
aber ich kann es ja noch hinzufügen. Ich habe leider eine 10cm Toleranz.
Das kann ich aber ja mit der Zeit anpassen. Trotzdem könntet ihr noch
einmal über meinen Code schauen?
Fabio M. schrieb:> Der Wert ist immer ungefähr 10cm höher.
Das ist mehr als erwartet - sehr viel mehr.
Versuche mal, diese beiden Zeilen zu vertauschen:
1
started=getUSTimePerCM();
2
while(!ECHO_SIGNAL);
Begründung: Die Breite des Echo-Pulses (High Pegel) soll gemessen
werden, nicht die Dauer vom Trigger bis zum Ende des Echo-Pulses.
Stefanus F. schrieb:> Bist du sicher, dass du keine Zustandsautomaten verwenden willst? Der> Vorteil wäre, dass du dann während der Messung noch was anderes machen> könntest.
Doch, aber ich bin mir noch nicht 100% sicher wie er funktionieren soll.
Wie Zustandsautomaten funktionieren können weiß ich. Aber nicht in
diesem Fall.
Ich habe bis jetzt leider erst einen Sensor zu testen, die anderen
werden noch geliefert.
Stefanus F. schrieb:> Bist du sicher, dass du keine Zustandsautomaten verwenden willst? Der> Vorteil wäre, dass du dann während der Messung noch was anderes machen> könntest.
Meinst du das als Zustandsautomat? Da es nur 2 Zustände hat habe ich es
mit einer if-Abfrage gelöst.
So ist gar nicht gut:
> if ((getUSTimePerCM() / 59)*1000 - lastChecked >= 60) {
Überlege mal, wie viel Rechenzeit wiegen dieser Zeile bei jedem
Schleifendurchlauf drauf geht. Du hast da eine Division und eine
Multiplikation bei jedem Schleifendurchlauf.
Besser so:
> if (getUSTimePerCM() - lastChecked >= 60*1000/58) {
Der Unterschied ist, dass in diesem Fall die Multiplikation und die
Division beide vom Compiler vorberechnet werden können. Die Kosten zur
Laufzeit sind 0.
60*1000/58 = 1034.
Das würde ich in eine definition auslagern, um dem Ausdruck einen
sprechenden Namen zu geben:
Stefanus F. schrieb:> So ist gar nicht gut:>> if ((getUSTimePerCM() / 59)*1000 - lastChecked >= 60) {>> Überlege mal, wie viel Rechenzeit wiegen dieser Zeile bei jedem> Schleifendurchlauf drauf geht.
Das ist primär erstmal kein Rechenzeitproblem. Solange die Rechenung
derart herbe Rundungsfehler produziert, gibt es noch ganz andere Gründe,
bei einer Ganzzahlrechnung über die Reihenfolge der Rechenschritte
nachzudenken.
Während du auf das Echo Signal wartest ist das Programm aber immer noch
blockiert. Stelle Dir mal vor, du möchtest parallel dazu die Tasten
eines Bedienfeldes abfragen oder eine LED blinken lassen. Das geht so
nicht. Da musst du noch mehr Zustände einführen. Ein Zustandsautomat
darf nur eine einzige Wiederholschleife enthalten, und zwar die
main-loop. Und delays sind tabu.
Ich habe das jetzt einfach schnell runter getippt. Könnte Logik- und
Syntaxfehler enthalten.
Der Triggerimpuls ist dieses mal 58µs lang, weil dies das kleinste
messbare Intervall mit dem Timer ist.