Forum: Mikrocontroller und Digitale Elektronik Suche besseren C-Code


von Johannes (Gast)


Lesenswert?

Hallo Leute,

ich habe einen Chip, den ich digital auslese. Man gibt ihm einen Takt
und er gibt 18bit aus. Ob high oder low lese ich am PORTpin DO1 von
PORTC ab. (es ist ein AS5045 chip, magnetic rotary encoder)

Ich habe definiert:

uint32_t data, aux;

for (dIndex=11; dIndex>=0; dIndex--)
{//readout position data

 aux = (PINC & (1<<DO1))>>DO1;
 // erzeugt ne 1 oder 0 als 32bitinteger je nach DO1-Pinstatus

 data |= aux<<dIndex;
 // 1 or 0 pushed left by dIndex positions

...
}

es funktioniert so wie ich will, sprich die Logik stimmt. Dies ist
meine "optimalste" Variante. Vielleicht geht es ja noch besser.
Wer kann mir sagen wie ich dies 2 Zeilen moeglichst hardwarefreundlich
umgestalte, sprich in moeglichst wenigen Prozessortakten abarbeite.
Laut Disassembler von AVR Studio entstehen fuer die beiden Zeilen
innerhalb der Schleife satte 29 Assemblerbefehle.

vielleicht hat jemand eine ganz andere Idee, wie man das verbessern
kann. Anderes Prinzip, etc...

danke schonmal,
mg,
Johannes

von Michael W. (miwitt001)


Lesenswert?

Ohne Garantie dass es stimmt (weil nicht getestet):

for (dIndex=11; dIndex>=0; dIndex--)
{//readout position data

    if(PINC & (1<<DO1))
        data|=1;
    data<<=1;

}

kleine Frage noch: data ist hier doch maximal 11 Bit lang (dIndex =
11), warum nimmst du dann keine 16 Bit Variable?? Oder überseh ich da
jetzt was?

mfg

von Geri (Gast)


Lesenswert?

Hallo Johannes

Vielleicht so, wenn ich richtig verstanden habe..

unsigned long Read_AS5045(void)
{
  unsigned long Data = 0;
  PinClk = 0;
  for (char i=...; i>= 0; i--)  // wie viele Bits..11, 18, ..?
  {
     PinClk = 1;   // setze Takt
     Data = Data | PinData;  //lies Daten
     PinClk = 0;   // reset Takt
     Data = Data << 1;  // Daten nach links schieben
  }
  return Data;
}

Ich weiss auch nicht wie effizient Dein Compiler die Anweisungen
übersetzt. Es sind aber lauter elementare Befehle

Beste Grüsse
Geri

von Geri (Gast)


Lesenswert?

.. jetzt ist mir Michael noch zuvor gekommen...:) Die Lösung sieht aber
auch etwas anders aus. Falls nur 11 Bit zum Auslesen sind, dann reicht
eine 16-Bit-Variable. Je nach Hardware müsste man vor dem Setzen von
PinClk die Daten zuerst auslesen. Kenne das Verhalten Deiner Hardware
aber nicht.
Je nach Compiler müsste man bei Data = Data| (unsigned long) PinData;
eine Typenkonvertierung vornehmen.

Die Abfrage, wie sie Michael gemacht hat, würde ich in diesem Fall
nicht machen, weil man vergleichen und dann u.U. springen muss.
Das ist nicht notwendig

Beste Grüsse
Geri

von Johannes (Gast)


Lesenswert?

Danke Geri, danke Michael!

meine 11 ist ein Fehler fuer meine Frage. Es haette 17 heissen sollen.
Die 11 ruehrt daher, dass ich versucht hatte die insgesamt 18bit in
zwei 16bit Variable zu lesen. Beim Schiften mussen dann jeweils nur 2
Register geschoben werden, nicht 4. Dabei kommt man schon auf eine
deutliche Zeitverbesserung, obwohl der C Code doppelt so lang ist, zwei
for-Schleifen benoetigt werden...

Geri: deine Loesung sieht nicht schlecht aus. PinData muesste ich nur
mit PINC2 ersetzen (in meinem speziellen Fall). Ich probiers mal aus.

Michael: in C ganz schoen kurz. Mal sehen was mein AVR-GCC draus
macht.

dank,
mg,
Johannes

von Geri (Gast)


Lesenswert?

Hallo Johannes

..bitte gerne. Lass uns das Ergebenis bitte wissen

Die binären operationen lassen sich selbstverständlich auch abkürzen,
wie Michael schön aufgezeigt hat.

Beste Grüsse
Geri

von Johannes (Gast)


Lesenswert?

Hallo,
habe jetzt nach dem (Geri-)Muster

"Data = Data | PinData;  //lies Daten"

und der mit (Michael-)Sptimierung :) folgende Zeile versucht:

data |= PINC2<<dIndex;

Aber es funktioniert nicht. Was fuer einen Variablentyp hat PINC2? Ich
denke, ohne eine zusaetzaliche Operation in der ich die 0x0001 erzeuge,
die ich verschieb und mit data veroder gehts doch nicht. Auch im AVR-GCC
Tutorial wird PINC2 nicht einfach abgefragt. Dort wird auch ein if
verwendet und dementsprechend agiert. Vielleicht ist Michaels Variante
doch simpler.

Johannes

von Geri (Gast)


Lesenswert?

.. wie gesagt, ich kenne Deinen Compiler nicht. Entweder es ist eine
Typenkonvertierung notwendig
PinData hat den Datentyp Bit

Data  |= (unsigned long) PinData; // auf diese Weise könnte es evtl.
gehen.

Bzgl. Schieben: Je nach verwendeter Hardware ist das Schieben nach
mehreren Bits nicht gereade effizient. Nicht jede CPU kann das.

Das oben aufgezeigte Prinzip stimmt aber.

Beste Grüsse
Geri

von Johannes (Gast)


Lesenswert?

Hallo,

also ich bin ein wenig "enttauscht" ... Ich dachte mein uC ist
besser...
Folgender Befehl fuer eine 16bit Variable

data |=1;

benoetigt 9 TaktZyklen. data<<=1 benoetigt gar 10. Tja, so ideal wie an
der Uni in einer Vorlesung ist die Welt eben nicht.

Mit der Methode von Michael redizier ich die Performance meines Codes
von 25 Taktzyklen auf 17. Immerhin einiges gewonnen. Wenn man noch mehr
ausquetschen will muss man warscheinlich alles selbst in Assembler
machen...
Die Variante mit PINC2 hab ich nicht ganz durchgechecked. Was ist PINC2
fuer ein Variablentyp. Klar ist, er wird um dIndex nach links geschoben.
 dIndex aendert sich bei jedem Durchlauf, der uC muss in Abhaengigkeit
davon mehr oder weniger schieben, das heisst er muss testen und dann
auch springen. Es tauchen RJMP Befehle auf. Insofern stimmt es nicht,
dass es nur elementare Befehle sind.

Jedenfalls vielen Dank Jungs!
Gruss aus HongKong,
Johannes

von Johannes (Gast)


Lesenswert?

Hi Geri,

das Prinzip ist natuerlich richtig. Ob mein ATMega16 effiziente
HArdware ist beim schieben um mehrere bits auf einmal weis ich nicht.

Jedenfalls muss man bei Michaels Methode immer genau eine Stelle
schieben. Das besser.

Johannes

von Geri (Gast)


Lesenswert?

Hallo Johannes

"Die Variante mit PINC2 hab ich nicht ganz durchgechecked. Was ist
PINC2 fuer ein Variablentyp. Klar ist, er wird um dIndex nach links
geschoben.  dIndex aendert sich bei jedem Durchlauf, der uC muss in
Abhaengigkeit davon mehr oder weniger schieben, das heisst er muss
testen und dann auch springen. Es tauchen RJMP Befehle auf. Insofern
stimmt es nicht, dass es nur elementare Befehle sind.
"


     PinClk = 1;   // setze Takt
     Data = Data | PinData;  //lies Daten
     PinClk = 0;   // reset Takt
     Data = Data << 1;  // Daten um 1 nach links schieben

PINClk ist vom Datentyp Bit. Er wird nicht um dIndex nach links
geschoben! Es wir immer um 1 bit geschoben. dIndex kommt im Code ja gar
nicht vor:)

Beste Grüsse
Geri

von Johannes (Gast)


Lesenswert?

"...
in der Eile des Gefechts,
sieht er nicht nach links und rechts!
..."

da hab ich ja einiges durcheinandergebracht. Sorry fuer die
Defaimierung.

" data |= PINC2; " bringt bei mir in der Simulation im AVR Studio
nicht das gewuenschte Ergebnis. Ob ich das PINC2 an oder ausklicke es
wird immer eine 2 addiert(0b00000010) und dann geschoben. Wo ist der
Datentyp Bit definiert? hab ich noch nie gehoert? hab immer nur mit
8bit Vars gearbeitet soweit ich mir dessen bewusst bin.

mg,
Johannes

von Geri (Gast)


Lesenswert?

Hallo Johannes

Datentyp bit wird nicht von allen Compiler unterstützt. Aber z.B. von
Keil (C167 etc...) oder Sourceboost (PIC).

In Deinem Fall steckt hinter PinC2 eine unsigned char Variable und s.w.
wird ein 8-bit-Port hier gleichzeitig eingelesen.

in diesem Fall ist das Demaskieren mit anschliessnder Und-Verknüpfung
notwndig - so wie Michael aufgezeigt hat
" if(PINC & (1<<DO1)) "

oder alternativ legst du Deinen Datenpin hardware-seitig auf Pin 0 und
schreibst:
Data = Data | (unsigned long) (DataPin & 0x01);
Dann kannst du dir immer noch die Abfrage sparen. Was nun effizienter
ist, musst du mal prüfen:)
Dieser Weg ist dann halt ein wenige unflexibler, weil der Eingangspin
am niedrigst signifikaten Bit liegen muss.

Beste Grüsse
Geri

von Michael Wilhelm (Gast)


Lesenswert?

Data = Data | (unsigned long) (DataPin & 0x01)
Abfrage von bit 0
Data = Data | (unsigned long) (DataPin & 0x80)
Abfrage von bit 7
Unflexibel ist dieser Weg doch nicht. Mit der Verundung kann man jedes
Bit rausfischen.

MW

von Geri (Gast)


Lesenswert?

Hallo Michael

In Johannes seinem Fall schon, weil er das untereste Bit laufend
einschieben muss. Wenn das bitt höhe signifikat ist, dann muss man es
zuerst nach rechts schieben. Das erfordert wieder etwas Zeit.

Beste Grüsse
Geri

von Michael W. (miwitt001)


Lesenswert?

Wenn du den AVR-Gcc verwendest ist PINC2 eine konstante zahl. Im Prinzip
steht das nur für (1<<2). Du könntest stattdessen auch PIND2 schreiben,
die Nummerierung ist unabhängig vom Port. Desahlb musst du um den
konkreten Wert herauszufidnen PORTC & PINC2 schreiben.
Versuch bei meinem Code mal das Data|=1 duch ein Data++ zu ersetzen.
Viell ist das noch ein bisschen schneller. Der Grund warum der Compiler
so viel Assembler erzeugt ist einfach der, dass AVRs (die Mega und At90
... Serie) intern alles mit 8 Bit Variablen rechnen. Da du aber 16 bzw
32 Bit Variablen benutzt gibt das einen relativ großen Overhead.

Wenn dich das "if" stört, versuchs mal damit:-)

for (dIndex=11; dIndex>=0; dIndex--)
{//readout position data

    data |= ( (PINC & (1<<DO1)) >> D01 );
    data<<=1;
}

mfg

von Rufus Τ. F. (rufus) Benutzerseite


Lesenswert?

Man kann auch ohne Schieben auskommen. Angenommen, man möchte im
Ergebnis Bit 0 setzen, wenn in der Quelle Bit 5 gesetzt ist:

   Data |= (Quelle & 0x20) ? 1 : 0;

(oder für die Bitzählfreunde unter uns)

   Data |= (Quelle & (1 << 5)) ? 1 : 0;

Wobei man sich natürlich schon die Effizienz des erzeugten Codes
anschauen sollte - wenn der Prozessor beim Schieben Äonen benötigt,
dann sehe ich Chancen für den ?-Operator.

Mit dem gleichen Konstrukt lassen sich auch beliebige andere
Kombinationen ohne Unterschiede in der Ausführungszeit hinbekommen:

Setzen von Bit 6 im Ergebnis bei gesetztem Bit 3 in der Quelle:

  Data |= (Quelle & 0x08) ? 0x40 : 0;

resp.

  Data |= (Quelle & (1 << 3)) ? (1 << 6) : 0;

Habe hier gerade keinen Compiler verfügbar, vielleicht mag ja mal wer
so eine Zeile übersetzen und die benötigten Takte vergleichen mit der
klassischen Schiebevariante.

von Geri (Gast)


Lesenswert?

Hallo Rufus

@Rufus: "Data |= (Quelle & 0x20) ? 1 : 0;"

Die Verwendung des  Bedingungsoperator "?" ist programmtechnisch
schön weil man unabhängig von der Pinbelegung wird, wenn man 0x20 als
Konstante definiert. Der Code wird auch schön kurz.

Im asm-code wird dann aber glaube ich wieder ein jump stehen.

Bin aber auch auf dei Performance gespannt...

Beste Grüsse
Geri

von peter dannegger (Gast)


Lesenswert?

Die Variante von Michael ist mit Abstand die effizienteste, da ein Bit
im Register zu setzen nur ein Befehl ist und somit der Eingangspin
direkt mit einer SBIC-Instruktion gelesen werden kann.

Ein Fehler ist aber drin, man muß zuerst schieben und dann verodern,
sonst ist das Ergebnis immer * 2.

Und irgendwo muß man auch noch den Taktpin togglen, je nach Flanke,
nach der der Datenpin gültig ist.


Allgemein sind direkte if ohne else-Zweig optimal (spart mindestens
einen Sprung ein).


Der ?: Operator wird vom AVR-GCC sehr schlecht optimiert, sieht also
nur schön kurz aus, isses aber nich.
Ein Mensch würde den "x |= 0" Zweig komplett wegoptimieren, der GCC
führt in brav aus.


Peter

von smay4finger. (Gast)


Lesenswert?

Ich frage mich aber, was es für einen Sinn macht den Code solange
umzustellen, bis er
 a) nicht mehr lesbar ist
 b) vom Compiler in Version x.y.z toll optimiert wird
 c) vom Compiler in Version y.z.x total anders verstanden wird

Warum dann nicht lieber gleich in Assembler? Wozu überhaupt eine
Hochsprache benutzen? In Wirklichkeit bescheißt man sich doch nur
selber. Wer soll das ganze Geraffel denn nachher wirklich verstehen?

Ich persönlich würde immer den sauberen C-Code bevorzugen. Sauberer
Code optimiert meiner Meinung nach auch am besten. Wenn der Compiler
das nicht sauber genug hinbekommt, dann sollte man den Teil lieber
gleich in Assembler codieren. Mein Tip: der bessere C-Code ist der, den
Mensch auch sofort verstehen kann.

mfg, Stefan.

von peter dannegger (Gast)


Lesenswert?

"Ich frage mich aber, was es für einen Sinn macht den Code solange
umzustellen, bis er
 a) nicht mehr lesbar ist
 b) vom Compiler in Version x.y.z toll optimiert wird
 c) vom Compiler in Version y.z.x total anders verstanden wird
"

Das würde ich auch nicht wollen, deshalb ja mein Tip mit Michaels
Code.
Der ist alles zugleich, kurz, gut zu verstehen und gut zu compilieren.


Nur weil man einen Code das erste mal hinschreibt, muß er noch lange
nicht besser verstehbar sein.
In der Regel ist aber kürzerer Code auch besser verstehbar.


Software-SPI ist ne Sache, die alle Nase lang vorkommt, da lohnt es
sich schon, den Code besser zu formulieren.


Peter

von Rufus Τ. F. (rufus) Benutzerseite


Lesenswert?

@Peter:

Das Optimierungsverhalten von gcc ließe sich also durch leichtes
Umstellen meines Ansatzes erhalten.

Statt

  Data |= (Quelle & 0x20) ? 1 : 0;

nunmehr

  if (Quelle & 0x20)
    Data |= 1;

Natürlich ist auch hier der generierte Code interessant, den ich
mangels akutem Zugriff auf einen Compiler wiederholt anderen zur
Untersuchung überlassen muss.

von Frank Simon (Gast)


Lesenswert?

Noch eine Meinung dazu:
Ich schreibe bei einem Zugriff auf einen speziellen Chip doch immer
gerne eine passende Assembler-Routine in eine eigene Datei.
Pro:
-> modular = wiederverwertbar
-> das geht mit dem GCC sehr schön
-> gleichzeitig Interrupts sperren, kannst Du besser oszilloskopieren
-> Timing absolut nachvollziehbar
-> brauchst Du nicht rätselzuraten über Compileroptimierungen
-> ist zwar nicht so schön portabel, aber man ist sowieso so nahe an
der Hardware, dass es nun mal sehr speziell ist.

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.