InhaltsverzeichnisÜberblickWie bekommt man man 20 PWM-Kanäle aus einem Atmel mit nur 2 oder 3 HW-Timern? Der grundsätzliche Ansatz ist, einen der Timer zu benutzen um per Interrupt eine größere Zahl von Ausgängen anzusteuern. So weit so gut, das ist schnell umgesetzt (im Folgenden will ich nur wesentliche Code-Abschnitte einbauen, um die Seite möglichst übersichtlich zu halten): erster Versuch in cvolatile unsigned char LEDs[20]; ISR ( TIMER2_COMP_vect ) { static unsigned char pwmpos; static unsigned char portb; PORTB = portb; pwmpos++; portb = ( ( LEDs[ 0] > pwmpos ) << 0 ) | ( ( LEDs[ 1] > pwmpos ) << 1 ) | ( ( LEDs[ 2] > pwmpos ) << 2 ) | ( ( LEDs[ 3] > pwmpos ) << 3 ) | ( ( LEDs[ 4] > pwmpos ) << 4 ) | ( ( LEDs[ 5] > pwmpos ) << 5 ) | ( ( LEDs[ 6] > pwmpos ) << 6 ); PORTB = portb | 0x80; } Die Variablen haben folgende Bedeutung: LEDs: ein Array, in dem die PWM-Werte der einzelnen Ausgänge hinterlegt werden. Es ist als volatile definiert, damit der Compiler vor jeder Benutzung den Wert auch wirklich aus dem Speicher holt und nicht etwa eine „lokale Kopie“ in einem Register verwendet. pwmpos: zählt hier einfach hoch und läuft nach 255 über, um von vorn anzufangen - der Referenzwert für die Frage, ob ein Pin an- oder ausgeschaltet werden soll. portb: enthält den Status, auf den PORTB zu Anfang des nächsten Interrupts gesetzt werden soll. Die Überlegung hierbei ist, dass man die Ausgänge möglichst deterministisch schalten möchte. Wenn man nicht von vornherein sagen kann, wie lange die Ermittlung des neuen Zustandes dauert, ist der pragmatische Ansatz, die Ausgänge erst zu setzen, wenn der nächste Interrupt ausgelöst wird. Der Code tut jetzt nichts anderes als den gewünschten PWM-Wert des jeweiligen Pins gegen die aktuelle PWM-Position zu vergleichen und das Ergebnis (1: der Ausgang muss an sein, 0: der ausgang muss aus sein) so weit nach links zu schieben, dass es am richtigen Pin landet. Das wird für alle Pins gemacht und danach alles zusammengeodert. Das letzte Bit wird hier zum Benchmarken des Zeitbedarfs immer am Ende eines jeden Zyklus auf 1 gesetzt, am Anfang des nächsten wird das Pin wieder ausgeschalten. Der Fehler, den man sich durch das zu frühe Schreiben der anderen Pins (geplant ist das eigentlich erst zum Beginn des nächsten Zyklus) verursacht, ist zumindest fürs Auge nicht auffällig. Was für mich daran interessant war - der vom Compiler Erzeugte Assemblercode für die Ermittlung der nächsten Pinzustände sieht so aus: 9e: 80 91 67 00 lds r24, 0x0067 a2: 28 17 cp r18, r24 a4: 08 f4 brcc .+2 ; 0xa8 <__vector_3+0x4a> a6: bf c0 rjmp .+382 ; 0x226 <__vector_3+0x1c8> a8: 90 e0 ldi r25, 0x00 ; 0 ... fa: 98 2b or r25, r24 fc: 9e 2b or r25, r30 fe: 97 2b or r25, r23 100: 96 2b or r25, r22 102: 95 2b or r25, r21 104: 94 2b or r25, r20 106: 93 2b or r25, r19 108: 90 93 64 00 sts 0x0064, r25 ... 226: 92 e0 ldi r25, 0x02 ; 2 228: 40 cf rjmp .-384 ; 0xaa <__vector_3+0x4c> In r18 steht pwmpos und in R24 landet hier der gewünschte PWM-Wert eines Pins. Ergibt der Vergleich (Zeile a2), dass das Pin 1 wird, wird nach 0x226 gesprungen (Zeile a6), um dort das Ausgangsregister auf den entsprechenden Wert zu setzen (0x02 für das zweite Pin). Soll das Pin 0 sein, wird der genannte Sprung ausgelassen (Zeile a4). Nachdem für jedes Pin ein Register mit der entsprechenden 0 oder 1 belegt wurde, werden alles zusammengeodert, wie im c-Code vorgeschrieben. Beim ersten flüchtigen Hinsehen habe ich die kurzen Sprünge (wie in Zeile a4) gesehen und gedacht, dass der die Bits in einer Schleife an die richtige Stelle schiebt … zweiter Versuch in c… das geht ja mal gar nicht, dachte ich und habe für einen anderen Ansatz einen Typ definiert, in dem ich in c die einzelnen Bits und das ganze Byte separat zugreifen kann: typedef union { unsigned char value; struct a { unsigned char a : 1; unsigned char b : 1; unsigned char c : 1; unsigned char d : 1; unsigned char e : 1; unsigned char f : 1; unsigned char g : 1; unsigned char h : 1; } bits; } bitfield; Der geänderte Code zur Ermittlung des Zustandes der einzelnen Pins sah nun so aus: static bitfield portb; PORTB = portb.value; pwmpos++; portb.bits.a = LEDs[ 0] > pwmpos; portb.bits.b = LEDs[ 1] > pwmpos; portb.bits.c = LEDs[ 2] > pwmpos; portb.bits.d = LEDs[ 3] > pwmpos; portb.bits.e = LEDs[ 4] > pwmpos; portb.bits.f = LEDs[ 5] > pwmpos; portb.bits.g = LEDs[ 6] > pwmpos; PORTB = portb.value | 0x80; Das Compilerergebnis sieht irgendwie nicht besser aus: 8e: 80 91 66 00 lds r24, 0x0066 92: 90 e0 ldi r25, 0x00 ; 0 94: 48 17 cp r20, r24 96: 08 f4 brcc .+2 ; 0x9a <__vector_3+0x3c> 98: 91 e0 ldi r25, 0x01 ; 1 9a: 20 91 64 00 lds r18, 0x0064 9e: 2e 7f andi r18, 0xFE ; 254 a0: 29 2b or r18, r25 a2: 20 93 64 00 sts 0x0064, r18 Hier wird jetzt für jedes einzelne Pin der Wert ermittelt (Zeilen 8e bis 98) und mit dem Wert für den ganzen Port verodert - deutlich schlechter als Version 1 … erster Versuch in c/inline AssemblerAuch ausgehend von der ersten Version - noch mit dem Gedanken an zu vermeidende Schleifen - habe ich mir ein stückchen Assembler überlegt, der das Drehen der Bits nicht in einer Schleife macht, sondern mit einer Art Duffs Device. Wenn man vorher inline-Assembler nur für NOPs verwendet hat (so wie ich), leistet das Inline Assembler Cookbook eine großartige Hilfe (vielen Dank von dieser Stelle aus). static inline unsigned char shift_left ( unsigned char para_in, unsigned char para_bitcount ) { asm volatile ( // store adress of 0: on stack "rcall 0f" "\n\t" "0:" "\n\t" // laod address of 0: from stack into Z r31 gets the // upper, r30 the lower byte of the adress (in words) "pop r31" "\n\t" "pop r30" "\n\t" "ldi r29, 14" "\n\t" // load immidiate into r29 "sub r29, %r[bitcnt]" "\n\t" // increase low adress byte by (14 - bitcnt) "add r30, r29" "\n\t" "adc r31, 0" "\n\t" // don't forget the carry bit "ijmp" "\n\t" // jump to the correct lsl "lsl %r[in]" "\n\t" // jump here for 7x shift "lsl %r[in]" "\n\t" // jump here for 6x shift "lsl %r[in]" "\n\t" // ... "lsl %r[in]" "\n\t" "lsl %r[in]" "\n\t" "lsl %r[in]" "\n\t" "lsl %r[in]" "\n\t" "mov %r[out], %r[in]" "\n\t" // output operand : [out] "=&r" (para_in) // input operands : [in] "r" (para_in), [bitcnt] "r" (para_bitcount) // clobber list - these objects are // modified by the code above : "r31", "r30", "r29" ); return para_in; } Und der zugehörige Code zur Ermittlung der Ausgangszustände sieht so aus: static unsigned char portb; PORTB = portb; pwmpos++; portb = ( shift_left ( LEDs[ 0] > pwmpos, 0 ) ) | ( shift_left ( LEDs[ 1] > pwmpos, 1 ) ) | ( shift_left ( LEDs[ 2] > pwmpos, 2 ) ) | ( shift_left ( LEDs[ 3] > pwmpos, 3 ) ) | ( shift_left ( LEDs[ 4] > pwmpos, 4 ) ) | ( shift_left ( LEDs[ 5] > pwmpos, 5 ) ) | ( shift_left ( LEDs[ 6] > pwmpos, 6 ) ); PORTB = portb | 0x80; Hier drin sind verschiedene Lapsi versteckt (z.B. hab ich den Eingabeparameter schon zur Ausgabe mitbenutzt, um das mov am Ende zu sparen, was aber noch drin steht …). Der Compiler hält sich an den Vorschlag, das Ergebnis braucht wesentlich mehr Programmspeicher und braucht ein vielfaches an Laufzeit. dritter Versuch in cNach dieser Enttäuschung habe ich die im Prinzip entscheidende Verbesserung in c implementiert - wieder ausgehend von der ersten Version - Zustand des ersten Pins ermitteln, eins weiterschieben, beim nächsten Pin fortsetzen: static unsigned char portb; PORTB = portb; pwmpos++; portb |= ( LEDs[ 6] > pwmpos ); portb <<= 1; portb |= ( LEDs[ 5] > pwmpos ); portb <<= 1; portb |= ( LEDs[ 4] > pwmpos ); portb <<= 1; portb |= ( LEDs[ 3] > pwmpos ); portb <<= 1; portb |= ( LEDs[ 2] > pwmpos ); portb <<= 1; portb |= ( LEDs[ 1] > pwmpos ); portb <<= 1; portb |= ( LEDs[ 0] > pwmpos ); PORTB = portb | 0x80; Das resultiert in folgendem Assembler-Code: 9e: 80 e0 ldi r24, 0x00 ; 0 a0: 23 17 cp r18, r19 a2: 08 f4 brcc .+2 ; 0xa6 <__vector_3+0x48> a4: 81 e0 ldi r24, 0x01 ; 1 a6: 98 2b or r25, r24 a8: 80 91 6b 00 lds r24, 0x006B ac: 99 0f add r25, r25 In r18 steht pwmpos, r19 enthält die gewünschte PWM und in r25 baut sich der nächste Wert für PORTB zusammen. Das ist bis hier die sympatischste Version, weniger Sprünge, weniger Befehle … zweiter Versuch in c/inline AssemblerHmm dachte ich … mal schauen, wie sich cp (assembler compare) auf das Statusregister auswirkt. Ich hatte das Carry-Flag im Auge, das man mit lsc (linksschieben mit Carry) direkt verwenden kann. Siehe da - wenn cp's Parameter 1 kleiner ist als Parameter 2 ist das Carry-Bit gesetzt - ideal. In Inline-Assembler sieht das dann so aus: static inline unsigned char cmp_carry_shift_left ( unsigned char para_io, unsigned char cmp_para_1, unsigned char cmp_para_2 ) { asm volatile ( // compare the parameters // (get the carry flag set/unset) "cp %r[cmp2], %r[cmp1]" "\n\t" // rotate carry bit into paramter "rol %r[out]" "\n\t" // output operand : [out] "+r" (para_io) // input operands : [in] "r" (para_io), [cmp1] "r" (cmp_para_1), [cmp2] "r" (cmp_para_2) ); return para_io; } man kommt ganz ohne Sprünge aus und braucht nur 2 Befehle bzw. Takte pro Pin plus Overhead :). static unsigned char portb; PORTB = portb; pwmpos++; portb = 0; portb = cmp_carry_shift_left ( portb, LEDs[ 6], pwmpos ); portb = cmp_carry_shift_left ( portb, LEDs[ 5], pwmpos ); portb = cmp_carry_shift_left ( portb, LEDs[ 4], pwmpos ); portb = cmp_carry_shift_left ( portb, LEDs[ 3], pwmpos ); portb = cmp_carry_shift_left ( portb, LEDs[ 2], pwmpos ); portb = cmp_carry_shift_left ( portb, LEDs[ 1], pwmpos ); portb = cmp_carry_shift_left ( portb, LEDs[ 0], pwmpos ); PORTB = portb | 0x80; compiliert zu folgendem vermutlich optimalen Assemblercode: 9e: 90 81 ld r25, Z a0: 39 17 cp r19, r25 a2: 88 1f adc r24, r24 Gewünschte PWM in r25 laden, mit pwmpos vergleichen und Ergebnisbit in r24 reinrotieren. ZusammenfassungCode wird offensichtlich nicht linear besser - besonders deutlich wird dasbei einem Blick auf folgende Messwerte (MCU: ATMega8, Takt: interner 8-MHz-Oszillator):
|