Warum verwendet AVR-Code die Bitverschiebung [geschlossen]

7

Bei der AVR-Programmierung werden Registerbits immer durch Verschieben von a 1nach links an die entsprechende Bitposition gesetzt - und durch ein Einsenkomplement derselben gelöscht.

Beispiel: Für einen ATtiny85 könnte ich PORTB, b 4 wie folgt einstellen :

PORTB |= (1<<PB4);

oder löschen Sie es so:

PORTB &= ~(1<<PB4);

Meine Frage ist: Warum wird es so gemacht? Der einfachste Code ist ein Durcheinander von Bitverschiebungen. Warum werden Bits als Bitpositionen anstelle von Masken definiert?

Der E / A-Header für den ATtiny85 enthält beispielsweise Folgendes:

#define PORTB   _SFR_IO8(0x18)
#define PB5     5
#define PB4     4
#define PB3     3
#define PB2     2
#define PB1     1
#define PB0     0

Für mich wäre es viel logischer, die Bits stattdessen als Masken zu definieren (wie folgt):

#define PORTB   _SFR_IO8(0x18)
#define PB5     0x20
#define PB4     0x10
#define PB3     0x08
#define PB2     0x04
#define PB1     0x02
#define PB0     0x01

Also könnten wir so etwas machen:

// as bitmasks
PORTB |=  PB5 |  PB3 |  PB0;
PORTB &= ~PB5 & ~PB3 & ~PB0;

um die Bits b 5 , b 3 und b 0 ein- bzw. auszuschalten. Im Gegensatz zu:

// as bit-fields
PORTB |=  (1<<PB5) |  (1<<PB3) |  (1<<PB0);
PORTB &= ~(1<<PB5) & ~(1<<PB3) & ~(1<<PB0);

Der bitmask Code liest viel deutlicher: gesetzte Bits PB5, PB3und PB0. Darüber hinaus scheint es Operationen zu sparen, da die Bits nicht mehr verschoben werden müssen.

Ich dachte, vielleicht wurde dies auf diese Weise getan, um die Allgemeinheit zu bewahren und den Portierungscode von einem n- Bit-AVR auf ein m- Bit (Beispiel 8-Bit auf 32-Bit) zu ermöglichen. Dies scheint jedoch nicht der Fall zu sein, da es #include <avr/io.h>sich um Definitionsdateien handelt, die für den Ziel-Mikrocontroller spezifisch sind. Selbst das Ändern von Zielen von einem 8-Bit-ATtiny zu einem 8-Bit-Atmega (bei dem sich die Bitdefinitionen beispielsweise syntaktisch von PBxzu ändern PORTBx) erfordert Codeänderungen.

Blair Fonville
quelle
3
Ich unterstütze das. Selbst die Verwendung des Allgegenwärtigen _BV(b)anstelle von (1<<b)macht die Dinge unnötig chaotisch. Ich definiere normalerweise Bit-Mnemonik mit _BV()z #define ACK _BV(1).
Calcium3000
2
Sobald festgestellt wird, dass der Compiler diese als dieselbe Konstante interpretiert, ist die Verwendung im Quellcode wirklich eine Frage der Präferenz. Tun Sie in Ihrem eigenen Code, was Sie für am klügsten halten. Halten Sie sich bei der Änderung bestehender Projekte an deren Traditionen.
Chris Stratton
3
"Da ein Bitmasken-Ansatz eindeutig zu besser lesbarem Endbenutzercode führen würde" - Ihre persönliche Meinung. Ich finde es viel klarer, Einsen und Nullen an die richtige Stelle zu verschieben, als raten zu müssen, ob mehrere Zahlen, die zusammenaddiert werden, Bitmasken sind oder nicht.
Tom Carpenter
3
@ TomCarpenter Interessant. Nun, vielleicht habe ich versehentlich eine meinungsbasierte Frage gestellt. In jedem Fall gab es einige gute Rückmeldungen. Aus einem eher (TI) DSP-Hintergrund stammend (wo Bitmaske die Norm ist), schien es nur eine so seltsame Syntax zu sein, dass ich dachte, es gäbe einen konkreten Grund dafür.
Blair Fonville
1
@BlairFonville Vielleicht wissen Sie das bereits, aber ARMs funktionieren genau so, wie Sie es beschreiben (mit Bitmasken).
Chi

Antworten:

7

Der einfachste Code ist ein Durcheinander von Bitverschiebungen. Warum werden Bits als Bitpositionen anstelle von Masken definiert?

Nein überhaupt nicht. Die Verschiebungen erfolgen nur im C-Quellcode, nicht im kompilierten Maschinencode. Alle Beispiele, die Sie gezeigt haben, können und werden vom Compiler zur Kompilierungszeit aufgelöst , da es sich um einfache konstante Ausdrücke handelt.

(1<<PB4) ist nur eine Möglichkeit, "Bit PB4" zu sagen.

  • Es funktioniert also nicht nur, es wird auch keine weitere Codegröße erstellt.
  • Für den menschlichen Programmierer ist es auch sinnvoll, die Bits nach ihrem Index (z. B. 5) und nicht nach ihrer Bitmaske (z. B. 32) zu benennen, da auf diese Weise fortlaufende Zahlen 0..7 verwendet werden können, um die Bits zu identifizieren, anstatt die umständliche Potenz von zwei (1, 2, 4, 8, .. 128).

  • Und es gibt noch einen anderen Grund (möglicherweise den Hauptgrund):
    Die C-Header-Dateien können nicht nur für C-Code verwendet werden, sondern auch für Assembler-Quellcode (oder Assembler-Code, der im C-Quellcode enthalten ist). Im AVR-Assembler-Code möchten Sie definitiv nicht nur Bitmasken verwenden (die durch Bitverschiebung aus Indizes erstellt werden können). Für einige Assembler-Anweisungen zur AVR-Bitmanipulation (z. B. SBI, CBI, BST, BLD) müssen Sie Bitindizes als unmittelbaren Operator in ihrem Befehls-Op-Code verwenden.
    Nur wenn Sie SFR-Bits anhand von Indizes identifizieren(nicht durch Bitmaske) Sie können solche Bezeichner direkt als unmittelbaren Operanden der Assembler-Anweisungen verwenden. Andernfalls mussten Sie zwei Definitionen für jedes SFR-Bit haben: eine Definition seines Bitindex (der z. B. als Operand in den oben genannten Assembler-Anweisungen zur Bitmanipulation verwendet werden kann) und eine Definition seiner Bitmaske (die nur für Anweisungen verwendet werden kann, bei denen das gesamte Byte verwendet wird manipuliert wird).

Quark
quelle
1
Ich verstehe das. Ich frage mich nicht, ob es funktioniert oder nicht. Ich weiß es tut. Ich frage, warum die Definitionen so geschrieben sind, wie sie sind. Für mich würde es die Lesbarkeit des Codes erheblich verbessern, wenn sie als Masken anstelle von Bitpositionen definiert würden.
Blair Fonville
5
Ich denke, diese Antwort geht am eigentlichen Punkt vorbei. Er spricht nie über Codeeffizienz oder Compiler. Es geht nur um Quellcode- Unordnung.
Pipe
4
@Blair Fonville: Es gibt keine einfache Möglichkeit, ein solches Makro zu definieren. Der Logarithmus musste zur Basis 2 berechnet werden. Es gibt keine Präprozessorfunktionalität zur Berechnung des Logarithmus. Das heißt, es könnte nur mit einem Tisch gemacht werden, und das wäre meiner Meinung nach eine sehr schlechte Idee.
Curd
2
@pipe: Ich spreche nicht darüber, weil ich es einfach nicht als "Codeverschmutzung" oder "Quellcode-Unordnung" betrachte (oder wie auch immer Sie es nennen möchten). Im Gegenteil, ich denke, es ist sogar nützlich, den Programmierer / Leser daran zu erinnern, dass die Konstante, die er verwendet, eine Zweierpotenz ist (und dies geschieht mit dem Shift-Ausdruck).
Curd
1
@RJR, Blair Fonville: Natürlich ist es leicht möglich, solche Makros zu definieren, ABER während die Verwendung einfacher Präprozessor-Definitionen in Ordnung ist, würde ich Präprozessor-Makros (auch bekannt als Präprozessor-Funktionen) nach Möglichkeit vermeiden, da sie das Debuggen (Durchlaufen des C-Quellcodes mit dem Debugger) extrem intransparent.
Curd
4

Vielleicht ist die Bitverschiebung nicht der einzige Anwendungsfall für die PB*Definitionen. Vielleicht gibt es andere Anwendungsfälle, in denen die PB*Definitionen direkt und nicht als Verschiebungsbeträge verwendet werden. Wenn ja, dann würde das DRY- Prinzip Sie dazu veranlassen, einen Satz von Definitionen zu implementieren, der für beide Anwendungsfälle (wie diese PB*Definitionen) verwendet werden kann, anstatt zwei verschiedene Sätze von Definitionen, die sich wiederholende Informationen enthalten.

Zum Beispiel habe ich eine Anwendung geschrieben, die Messungen von bis zu 8 ADC-Kanälen durchführen kann. Es verfügt über eine Schnittstelle zum Starten einer neuen Messung, in der Sie mehrere Kanäle über ein 8-Bit-Feld angeben können, ein Bit für jeden Kanal. (Die Messungen werden parallel durchgeführt, wenn mehrere Kanäle angegeben sind.) Dann verfügt es über eine andere Schnittstelle, die die Messergebnisse für einen einzelnen Kanal zurückgibt. Eine Schnittstelle verwendet also die Kanalnummer als Verschiebung in ein Bitfeld und die andere Schnittstelle verwendet die Kanalnummer direkt. Ich habe eine einzelne Aufzählung definiert, um beide Anwendungsfälle abzudecken.

typedef enum
{
    CHANNEL_XL_X = 0,
    CHANNEL_XL_Y = 1,
    CHANNEL_XL_Z = 2,
    CHANNEL_G_X = 3,
    CHANNEL_G_Y = 4,
    CHANNEL_G_Z = 5,
    CHANNEL_AUX1 = 6,
    CHANNEL_AUX2 = 7
} ChannelNum;

struct MeasurementResult;

void StartMeasurement(uint8_t channel_mask);
MeasurementResult ReadMeasurementResult(ChannelNum channel_num);

main
{
    ...

    StartMeasurement( (1 << CHANNEL_XL_X) | (1 << CHANNEL_XL_Y) | (1 << CHANNEL_XL_Z) );

    meas_result_x = ReadMeasurementResult(CHANNEL_XL_X);
    meas_result_y = ReadMeasurementResult(CHANNEL_XL_Y);
    meas_result_z = ReadMeasurementResult(CHANNEL_XL_Z);
}
kkrambo
quelle