Hallo zusammen,
vor kurzem habe ich eine SPI in Software umgesetzt. Die Frage, die ich
habe betrifft spezifisch das setzen/löschen der Bits auf dem Data-Port
während des Sendevorgangs.
Hier im Forum sehe ich fast immer die Lösung, mit einer Maske zu
arbeiten, die dann in jedem Schleifendurchlauf geshiftet wird um somit
das Bit zu maskieren, das gesendet werden soll. Beispielsweise so
(nächstbestes Beispiel, das ich gefunden habe):
1
void display_out(unsigned char out)
2
{
3
unsigned char mask = 0x80;
4
5
while(mask)
6
{
7
DISPLAY_PORT &= ~(1 << CLK);
8
if(mask & out)
9
{
10
DISPLAY_PORT |= (1 << SDA);
11
}
12
else
13
{
14
DISPLAY_PORT &= ~(1 << SDA);
15
}
16
DISPLAY_PORT |= (1 << CLK);
17
mask = mask >> 1;
18
}
19
}
Ich selbst habe es gelöst durch
1
...
2
if(data&(1<<i))
3
...
Das funktioniert auch soweit. Ich frage mich daher, warum man den
"Umweg" über die Maske macht, also extra eine Variable einführt, die
dann ebenfalls durch das Shiften CPU-Zyklen verbrät.
Kann mir da jemand weiterhelfen?
viele Wege führen nach Rom...
Unterschied der beiden Methoden:
Du schiebst die Bits von rechts nach links raus, wenn du anderstrum
schieben willst geht das natürlich auch.
Allerdings hast du schon mal die Anzahl der Schiebeoperationen gezählt,
die für einen kompletten Schleifendurchlauf zusammenkommen?
28 versus 7 - damit ist die erste Methode, trotz einer temporären
Variable, sicherlich schneller.
Christian
Hallo elpaco,
du brauchst so oder so eine Variable, mit der du die Maske erstellt.
Deine Lösung sieht auf den ersten Blick vielleicht "billiger" (in Bezug
auf CPU-Zyklen) aus, jedoch benutzt du ja auch eine Variable (dein "i" -
vermutlich von einer for-Schleife).
Ich kann dir jetzt nicht genau sagen, wie dein Compiler optimiert, aber
die Lösung mit der Masken-Variablen sieht für mich so als, als ob sie
effizienter wäre, da die Maske gleichzeitig auch die Schleifenbedingung
darstellt.
KURZE (!) Gegenüberstellung der beiden Varianten:
Maske:
-INIT
-Schleifenbedingung überprüfen
-Pinwackeln
-Rechtsshift
-Rücksprung zur Schleife
for-Schleife:
-INIT
-Schleifenbedingung überprüfen
-Linksshift um i Stellen
-Pinwackeln
-Zählvariable verändern
-Rücksprung zur Schleife
Viele Grüße
Okay, das heißt das (1<<i) ist im Endeffekt nichts anderes als eine
Variable anzulegen und sie i-mal zu shiften. Und durch die Maske spart
man sich das, jeden Durchlauf neu zu shiften. Man muss nur insgesamt N
mal shiften und nicht
Der teure Teil an Deiner Lösung ist das "1<<i". Auch wenn es harmlos
aussieht, die meisten Mikrocontroller können den Wert nicht mit einem
Maschinenbefehl berechnen. Ohne Barrel-Shifter kann der Prozessor pro
Befehl nur um eine Stelle nach rechts/links shiften.
Der Compiler muss also eine Schleife bauen mit einer temporären Variable
(Anfangswert 1), die er i-Mal um eine Stelle nach links shiftet. Und das
in jedem Schleifendurchlauf neu. Das braucht deutlich mehr Zeit.
Falls der Compiler perfekt optimiert, könnte am Ende der gleiche Code
wie in der Variante mit der expliziten Maske rauskommen. Drauf verlassen
würde ich mich aber nicht.
>Ich frage mich daher, warum man den>"Umweg" über die Maske macht, also extra eine Variable einführt, die>dann ebenfalls durch das Shiften CPU-Zyklen verbrät.
Da kann man dir eigentlich nur den Tip geben mal den erzeugten
Assemblercode anzusehen. Zusätzlich dann vor der Schleife mal
einen Pin setzen und danach löschen. Das ganze dann auf einem
Osci ansehen oder mit dem Simulator mal CPU Zyklen zählen lassen.
Dann siehst du ob es einen Unterschied gibt oder nicht.
AVR Studio benutzen. beide Varianten Programmieren. Im Simulator laufen
lassen und das Assemblerfenster anschauen. Am Ende bekommst Du noch die
komplette Zeit deiner Routine angezeigt. Die Sauberste Lösung ist
übrigens es direkt selber in Assembler zu programmieren und einbinden,
wenn man meint es sei Zeitkritisch.
Fabian O. schrieb:> Der teure Teil an Deiner Lösung ist das "1<<i". Auch wenn es> harmlos> aussieht, die meisten Mikrocontroller können den Wert nicht mit einem> Maschinenbefehl berechnen. Ohne Barrel-Shifter kann der Prozessor pro> Befehl nur um eine Stelle nach rechts/links shiften.
so sieht es aus :-)
SAMUEL schrieb:> Die Sauberste Lösung ist> übrigens es direkt selber in Assembler zu programmieren und einbinden,> wenn man meint es sei Zeitkritisch.
Nä, wat hammer gelacht.
1
uint8_tshift_io(uint8_tb)// send / receive byte
2
{
3
uint8_ti;
4
5
SPI_CLK_DDR=1;// set as output
6
SPI_MOSI_DDR=1;
7
8
for(i=8;i;i--){// 8 bits
9
SPI_MOSI=0;
10
if(b&0x80)// high bit first
11
SPI_MOSI=1;
12
b<<=1;
13
SPI_CLK=1;
14
if(SPI_MISO_PIN)
15
b++;
16
SPI_CLK=0;
17
}
18
returnb;
19
}
Und hier das Listing:
1
uint8_tshift_io(uint8_tb)// send / receive byte
2
{
3
uint8_ti;
4
5
SPI_CLK_DDR=1;// set as output
6
2e:b89asbi0x17,0;23
7
SPI_MOSI_DDR=1;
8
30:b99asbi0x17,1;23
9
32:98e0ldir25,0x08;8
10
11
for(i=8;i;i--){// 8 bits
12
SPI_MOSI=0;
13
34:c198cbi0x18,1;24
14
if(b&0x80)// high bit first
15
36:87fdsbrcr24,7
16
SPI_MOSI=1;
17
38:c19asbi0x18,1;24
18
b<<=1;
19
3a:880faddr24,r24
20
SPI_CLK=1;
21
3c:c09asbi0x18,0;24
22
if(SPI_MISO_PIN)
23
3e:b299sbic0x16,2;22
24
b++;
25
40:8f5fsubir24,0xFF;255
26
SPI_CLK=0;
27
42:c098cbi0x18,0;24
28
uint8_ti;
29
30
SPI_CLK_DDR=1;// set as output
31
SPI_MOSI_DDR=1;
32
33
for(i=8;i;i--){// 8 bits
34
44:9150subir25,0x01;1
35
46:b1f7brne.-20;0x34<__CCP__>
36
if(SPI_MISO_PIN)
37
b++;
38
SPI_CLK=0;
39
}
40
returnb;
41
}
42
48:0895ret
Bin mal gespannt, wie man das in Assembler noch besser optimiert.
Haha jenau ich lach mit.
1. War das Allgemein gemeint.
2. Eine gut geschriebene Assemblerfunktion ist <= Compiler
3. Komme ich zum rausschieben eines Bytes via SPI auf 12 instructions,
bei deinem Compiler Beispiel zähle ich 14.
Ich muss mich korrigieren es sind bei mir sogar nur 10 Instrucktion's
die letzten beiden:
sbi PORTB, LCK
cbi PORTB, LCK
gehören schon nicht mehr zu SPI-Routine. Also noch mal mehr hahaha...
SAMUEL schrieb:> Ich muss mich korrigieren es sind bei mir sogar nur 10 Instrucktion's
Ich hätte gedacht, es wäre aus den Kommentaren ersichtlich, daß meine
Routine etwas mehr macht.
Die zusätzlichen Befehle setzen die Pins auf Ausgang und lesen MISO ein.
Oft muß man einen SPI-Slave ja auch auslesen.
Wenn ich das weglasse, sinds auch 10.
SAMUEL schrieb:> Warum jedes mal aufs neue als Ausgang konfigurieren. In der Regel halten> die ihren Zustand.
Dann nimms raus, wenn dir das nicht gefällt.
Das war doch gar nicht der springende Punkt, den PeDa angesprochen hat.
Im Endeffekt ist es dann auch ineffizient, bspw ADC-Register mit z.B.
(1<<ADSC) zu setzen, sofern diese nicht I/O sind und somit sbi/cbi
können? Da wäre dann ein immediate-Wert effizienter, wenn ich das
richtig verstehe?
M. S. schrieb:> Im Endeffekt ist es dann auch ineffizient, bspw ADC-Register mit z.B.> (1<<ADSC) zu setzen, sofern diese nicht I/O sind und somit sbi/cbi> können? Da wäre dann ein immediate-Wert effizienter, wenn ich das> richtig verstehe?
Der Compiler macht dir daraus schon einen immediate-Wert, solange beide
seiten des << Operators konstant sind.
M. S. schrieb:> Im Endeffekt ist es dann auch ineffizient, bspw ADC-Register mit z.B.> (1<<ADSC) zu setzen
Konstante Ausdrücke werden schon zur Compilezeit ausgerechnet.
Du kannst daher ohne Bedenken Konstanten in float definieren, z.B.
Faktoren für die Berechnung von Zeiten, Spannungen, Temperaturen usw..
Aber sicherheitshalber vor der Verwendung noch nach Ganzzahl (uint16_t)
casten.
Peter Dannegger schrieb:> Du kannst daher ohne Bedenken Konstanten in float definieren, z.B.> Faktoren für die Berechnung von Zeiten, Spannungen, Temperaturen usw..> Aber sicherheitshalber vor der Verwendung noch nach Ganzzahl (uint16_t)> casten.
Wobei hier das Klammern noch wesentlich wichtiger als das casten ist.
z.B. wird
1
2
#define A 47.11
3
#define B 8.15
4
floaty=x*A/B;
nicht durch eine Multiplikation mit der Konstante 5.78 (=47.11/8.15)
ersetzt (musste ich schmerzhaft lernen). Der resultierende
Assembler-Code enthält eine Multiplikation und eine (teure) Division
Der Code wird wesentlich schneller, wenn man nur eine Klammer setzt:
1
2
#define A 47.11
3
#define B 8.15
4
doubley=x*(A/B);
Hier rechnet der Compiler wirklich eine Konstante und erzeugt nur eine
Multiplikation.
Wichtig: das ist kein Fehler des Compilers, aufgrund der begrenzten
genauigkeit von Fließkommazahlen (egal ob float oder double) muss er
richtigerweise so vorgehen.