Forum: Mikrocontroller und Digitale Elektronik schnelle Kommunikation über UART


Announcement: there is an English version of this forum on EmbDev.net. Posts you create there will be displayed on Mikrocontroller.net and EmbDev.net.
von Nikolas B. (physikant)


Lesenswert?

Hallo,

ich habe vor, 16 Bit Datenpakete über den UART eines AtMega168 zu 
transportieren. Die Daten stammen von einem ADC von TI. Dieser sampelt 
alle 25µS, die ich auch so gut es geht ausnutzen will. Ich betreibe die 
Schnittstelle mit 384000 baud.
Hier der Quelltext.
1
#define F_CPU 18432000UL 
2
#define BAUD 384000UL 
3
#define SPEED ((F_CPU+BAUD*8)/(BAUD*16)-1)
4
#include <util/delay.h>
5
#define nop() __asm volatile ("nop")
6
7
volatile uint8_t active = 0;
8
9
void uart_init(void)
10
{
11
  UBRR0H = SPEED >> 8;
12
  UBRR0L = SPEED & 0xFF;
13
  UCSR0A |= (1<<TXC0);
14
  UCSR0B = (1<<RXEN0)|(1<<TXEN0)|(1<<RXCIE0);
15
  sei();
16
}
17
18
void uart_puti16(uint16_t val)
19
{
20
21
  while ( !( UCSR0A & (1<<UDRE0)) )
22
  nop();
23
  UDR0 = val >> 8;
24
  while ( !( UCSR0A & (1<<UDRE0)) )
25
  nop();
26
  UDR0 = val & 0xFF;
27
}
28
29
int main (void) {
30
  
31
  DDRC |= (1 << PC5);
32
  uart_init();
33
  ADC_init();
34
35
  DDRD  &= ~(1 << PD2);
36
37
  while(1)
38
  {
39
40
    if(active == 1)
41
    {  
42
      uart_puti16(readADC());
43
    }        
44
  }
45
  return 0;             
46
}
47
48
ISR(USART_RX_vect)
49
{
50
    if(UDR0 == 'a')
51
    {
52
      uart_puti16(readADC());
53
    }
54
    if(UDR0 == 's')
55
    {
56
      active = 1;
57
    }
58
    if(UDR0 == 'e')
59
    {
60
      active = 0;
61
    }
62
}

Wenn ich PC-seitig ein Programm verwende, dass in einer for-Schleife ein 
"a" sendet und dann 2 Bytes ausliest, erhalte ich stets korrekte Daten, 
allerdings mit nur etwa 500Hz, ich möchte allerdings mindestens 15kHz 
erreichen.
Wenn ich nun ein "s" sende und dann konstant immer 2 Bytes einlese, geht 
es schief. Die Datenpakete fangen in der Mitte des einen Datenpakets an 
und enden in der Mitte des anderen.
Ich habe in die While-Schleife der main-Funktion diverse Werte von 
_delay_us() und _delay_ms() eingebaut. Bei 1ms tritt der Fehler nicht 
mehr auf, bei 5-10µs verschiebt sich die Position, bei der die Daten in 
der Mitte eines Pakets anfangen.
Ich habe zunächst vermutet, dass es an der PC-Seite liegt, aber sowohl 
mein selbstgeschriebenes Programm, als auch realterm ergeben diesen 
Fehler.
Woran könnte das liegen?

Danke im Vorraus!
Grüße
   Nikolas

von Peter II (Gast)


Lesenswert?

du kannst doch nicht einfach das Register mehrfach auslesen!

if(UDR0 == 'a')
    {
      uart_puti16(readADC());
    }
    if(UDR0 == 's')
    {
      active = 1;
    }
    if(UDR0 == 'e')
    {
      active = 0;
    }

das macht man nicht!

uint8_t tmp = UDR0;

if ( tmp == 'a' )

> while ( !( UCSR0A & (1<<UDRE0)) )
>  nop();
was soll das nop hier, ich dachte es soll schnell gehen?

von Peter II (Gast)


Lesenswert?

vegiss das mit dem nop - es fehlt nur die notwendige einrückung dann ist 
es klar.

von Nikolas B. (physikant)


Lesenswert?

Ok, korrigier ich schnell. Sollte aber am Problem nix ändern.

von Pako (Gast)


Lesenswert?

1. Du rufst die Funktion "uart_puti16()" sowohl aus dem main() als auch 
aus der ISR heraus auf, ohne daß diese reentrant oder 
interruptverriegelt wäre.
2. Wenn Du "uart_puti16()" aus der ISR heraus aufrufst, ist Dein 
Empfänger in dieser Zeit blockiert und kann keine Daten empfangen.

von Karl H. (kbuchegg)


Lesenswert?

Nikolas B. schrieb:

> Ich habe in die While-Schleife der main-Funktion diverse Werte von
> _delay_us() und _delay_ms() eingebaut. Bei 1ms tritt der Fehler nicht
> mehr auf, bei 5-10µs verschiebt sich die Position, bei der die Daten in
> der Mitte eines Pakets anfangen.
> Ich habe zunächst vermutet, dass es an der PC-Seite liegt, aber sowohl
> mein selbstgeschriebenes Programm, als auch realterm ergeben diesen
> Fehler.

Das schliesst immer noch nicht aus, dass die UART-Schicht am PC nicht 
mitkommt. Auf dem PC ist die ganze Sache ja nicht so einfach wie auf dem 
AVR. Da liegen einige Softwareschichten und da du kein Handshake benutzt 
hat der PC keine Möglichkeit den µC mal kurz ein wenig einzubremsen.

Allerdings: Bytes einfach so rauszublasen ist meistens sowieso ein 
Hazardspiel. Ohne eine Form von Primitiv-Protokoll wirst du nicht weit 
kommen.

Vorschlag:
Da der ADC Wert nur 10 Nutzbits hat, verschieb die 'Trennlinie' an der 
du den 16 BIt Wert in 2 Bytes auftrennst. Das LowByte bildet sich aus 
den 7 unteren Bits des Wertes. Das High-Byte aus den 3 höherwertigen.
Was hast du dadurch gewonnen?
Du kriegst 1 Bit frei - das höchstwertige Bit - welches du als Kennung 
benutzen kannst ob es sich bei dem Byte um das High-Byte oder das 
Low-Byte handelt. Beim einen ist es auf 1 gesetzt, beim anderen ist es 
0. Der Empfänger kann dann anhand des Bytes entscheiden was er damit tun 
muss, bzw. fehlende Bytes erkennen und die Messung verwerfen.

von Nikolas B. (physikant)


Lesenswert?

Der Aufruf im ISR ist im Prinzip unnötig. Man könnte ihn komplett 
streichen, da ich die Methode "a" senden - 2 Byte empfangen - "a senden 
- usw. nur testweise implementiert hatte.

von Karl H. (kbuchegg)


Lesenswert?

Allerdings

>  erhalte ich stets korrekte Daten, allerdings mit nur etwa 500Hz

das erscheint mir dann doch ein bischen wenig.

Das Problem könnte folgendes sein:
Dein Empfänger kann im Prinzip wesentlich schneller empfangen. Gibst du 
allerdings die Werte sofort irgendwo aus, dann bremst diese Ausgabe 
alles andere aus. Dein Problem ist in diesem Fall nicht die Serielle, 
sondern das Hinpinseln der Werte auf den Monitor.

von Pako (Gast)


Lesenswert?

Nikolas B. schrieb:
> Der Aufruf im ISR ist im Prinzip unnötig.

Vorallem ist er gefährlich. Mit Interrupts zu arbeiten ist nicht ohne.
Gerade so Sachen wie Warteschleifen in ISRs (bzw. Aufruf von Funktionen 
mit Warteschleifen) ist kein guter Stil, sofern es überhaupt 
funktioniert.

Guckst Du
http://www.mikrocontroller.net/articles/Interrupt

von Nikolas B. (physikant)


Lesenswert?

Tut mir leid, dass ich das nicht erwähnt habe, dachte, es wäre nicht 
nötig. Der ADC hat 16bit, es ist ein ADS7825 von TI.

von Karl H. (kbuchegg)


Lesenswert?

Jungs, bitte!

Das war ein Test! Er hat sich halt eine quick&dirty Lösung eingebaut, 
mit der er testen kann, ob ADC und UART prinzipiell funktionieren.

Sobald das Kommando für Dauersampeln vom PC kommt, spielt die ISR keine 
große Rolle mehr.

Ja, das ist kein guter Stil in der ISR. Aber trotzdem bellst du hier den 
falschen Baum an.
Wenn dein Auto statt der üblichen 120 nur noch 80 fahren kann, bringt es 
nichts die halb durchgerostete Anhängerkupplung zu bemängeln. Ja, sie 
ist durchgerostet und ja das sollte nicht sein. Aber mit dem Problem der 
defekten Zylinderkopfdichtung hat das nichts zu tun.

von Nikolas B. (physikant)


Lesenswert?

@Pako
Dann denke man sich den Abschnitt mit "a" weg.

@kbuchegg
PC-Seitig läuft das Auslesen in einem eigenen Thread, der konstant Werte 
in ein Array schreibt. Ein anderer Thread liest dieses Array aus und 
stellt es dar.
Soll ich die PC-Seite posten? Muss da allerdings warnen, ist in c++/CLI 
geschrieben und verwendet .net-Methoden ...

von Karl H. (kbuchegg)


Lesenswert?

Die readADC ist tempomässig über jeden Zweifel erhaben?
(Zur Probe würd ich mal einfach konstante Werte schicken
1
  while(1)
2
  {
3
4
    if(active == 1)
5
    {  
6
      uart_puti16( 4711 );
7
    }        
8
  }

wenn da der Versatz immer noch da ist, wäre mein heißer Kandidat die 
PC-Seite.
)

von Nikolas B. (physikant)


Lesenswert?

Hier die PC-Seite:
1
static int werte[1000000];
2
static int counter = 0;
3
static int pos = 0;
4
5
void reader(void)
6
{
7
  SerialPort^ port;
8
  Parity p = (Parity)Enum::Parse(Parity::typeid, "None");
9
  StopBits s = (StopBits)Enum::Parse(StopBits::typeid, "1");
10
  port = gcnew SerialPort("COM16",384000,p,8,s);
11
  port->Open();
12
  unsigned int i = 0;
13
  unsigned int j = 0;
14
  port->Write("s");
15
  while(true)
16
  {
17
    i = port->ReadByte();
18
    j = port->ReadByte();
19
    werte[pos] = j + (i*256);
20
    pos++;
21
    if(pos == 1000000)
22
    {
23
      pos = 0;
24
      counter++;
25
    }
26
  }
27
}
28
29
in main dann:
30
Thread^ readThread = gcnew Thread(gcnew ThreadStart(reader));
31
readThread->Start();
32
33
und dann bei Bedarf Werte auslesen.

von Nikolas B. (physikant)


Lesenswert?

Wenn ich 4711 sende, geht es. Allerdings ist der ADCreader so ne Sache, 
das war schon eine schwere Geburt, bis ich den am laufen hatte. Es ist 
der ADS7825 von TI mit 16bit und 25µS Conversion-Time, hier der Code:
1
uint16_t readADC(void)
2
{
3
  DCLK_PORT &= ~(1<<DCLK_PIN); //Clk-Signal beginnen
4
  uint16_t data = 0;  //hier kommen die Daten rein
5
  STARTSIG_PORT |= (1<<STARTSIG_PIN); //Konversion einleiten
6
  DCLK_PORT |= (1<<DCLK_PIN);  //clk-Signal fortsetzen
7
  DCLK_PORT &= ~(1<<DCLK_PIN);  
8
  DCLK_PORT |= (1<<DCLK_PIN);  // nach 1 1/2 Takten kommen die Daten           
9
  for(int i = 15; i >= 0; i--) //16 Datenbits auslesen
10
  {
11
    DCLK_PORT &= ~(1<<DCLK_PIN); //Flanke wechseln
12
    data |= ((DATASIG_PORT&(1<<DATASIG_PIN))<<i); // Daten einlesen
13
    DCLK_PORT |= (1<<DCLK_PIN); // Flanke wechseln
14
  }
15
  STARTSIG_PORT &= ~(1<<STARTSIG_PIN); // Konversion beenden
16
  return data; 
17
}
Hier wird während der Konversion das Ergebnis der letzten Konversion 
ausgelesen.

von Pako (Gast)


Lesenswert?

Nikolas B. schrieb:
> der ADS7825 von TI mit 16bit und 25µS Conversion-Time

Sag mal, bist Du Dir sicher, daß es überhaupt ein Übertragungsfehler ist 
und nicht ein Problem beim ADC?

Funktioniert dies hier:
1
  uint16_t x = 0;
2
  while(1)
3
  {
4
    if(active == 1)
5
    {  
6
      uart_puti16( x++);
7
    }        
8
  }

Beim ADC müßtest Du die Sample-Zeit und die Conversion-Zeit warten, 
bevor Du das Ergebnis auslesen darfst, ansonsten ist es ungültig. In 
Deiner readADC()-Funktion startest Du eine Conversion und liest direkt 
das Ergebnis aus, ohne Wartezeit.
Noch was: braucht der ADC vielleicht ein ChipSelect-Wechsel zwischen 
zwei Wandlungen?

von Nikolas B. (physikant)


Lesenswert?

Irrtum, ich beende die Konversion zu Beginn des ADC-readers. Gestartet 
wird sie an dessen Ende. Wie ich im Beitrag zuvor schon geschrieben 
habe, lassen sich statische Werte problemlos übertragen, es liegt also 
schon am ADC. Ein Chipselect ist nicht nötig, sonst würde es ja sonst 
nicht gehen.
Was ich festgestellt habe, ist, dass wenn ich folgendes Einfüge:
1
uart_puti16(readADC());
2
_delay_us(17);
die Übertragung funktioniert. Das macht das ganze etwas seltsam, denn 
offensichtlich ist irgend etwas zu langsam. Mit dieser Zeile braucht ein 
kompletter Zyklus etwa 72µS.
Nun kann man das mal durchrechnen:
Auslesen des ADC (grob geschätzt): 17µS
Übertragen der Daten (19/384000): 50µS
Nun hat der ADC zwischen zwei Aufrufen seiner Auslese-Funktion genug 
Zeit zur Konversion, da er 25µS braucht, aber 50µS hat. Nun stellt sich 
die Frage, warum diese 17 zusätzlichen µS notwendig sind?
Jedenfalls werd ich mich wohl (wenn sonst niemand noch eine Idee hat) 
wohl mit den 72µS zufrieden geben und akzeptieren, dass ich nur 14kHz 
statt den angestrebten 20kHz zur Verfügung habe.
Schade.
Danke auf jeden Fall an alle, die sich um eine Hilfe bemüht haben!!
Grüße
   Nikolas

von spess53 (Gast)


Lesenswert?

Hi

Dumme Frage: Warum nimmst du nicht das Hardware-SPI?

MfG Spess

von Nikolas B. (physikant)


Lesenswert?

Ein Blick ins Datenblatt zeigt: der ADC verwendet kein SPI, auch wenn TI 
das vielleicht behauptet.

von Pako (Gast)


Lesenswert?

Nikolas B. schrieb:
> uint16_t readADC(void)
> {
>   (...)
>   STARTSIG_PORT |= (1<<STARTSIG_PIN); //Konversion einleiten
>   (...)
>   STARTSIG_PORT &= ~(1<<STARTSIG_PIN); // Konversion beenden
>   (...)
> }

Nikolas B. schrieb:
> Irrtum, ich beende die Konversion zu Beginn des ADC-readers.

Tatsächlich? Ich habe aus den Kommentaren das Gegenteil geschlossen.

von spess53 (Gast)


Lesenswert?

Hi

>Ein Blick ins Datenblatt zeigt: der ADC verwendet kein SPI, auch wenn TI
>das vielleicht behauptet.

Für mich sieht das wie SPI Mode1 aus.

MfG Spess

von Nikolas B. (physikant)


Lesenswert?

@Pako
Mit Irrtum meinte ich, dass meine Kommentare falsch sind. Der Pin auf 
LOW startet die Konversion.

@spess
Leider kenne ich mich mit dem SPI-Protokoll nicht gut genug aus, um es 
zu erkennen. Jedenfalls wusste ich nicht, wie das mit dem Einleiten 
einer Konversion (und dem entsprechenden Timing zum Auslesen) 
funktionieren soll, wenn ich Hardware-SPI verwende. Würde mich da aber 
über etwas Hilfe freuen, wenn das doch funktionieren sollte :)

Grüße
   Nikolas

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
Noch kein Account? Hier anmelden.