Skip to content

MSP430 - Assembler und Code-Optimierung

  • Motivation: Entwickeln eines Gefühls, welche C-Befehle wie viel Laufzeit im MSP430 benötigen.
  • Dazu muss kenngelernt werden:
    • Wie führt der MSP430 Befehle aus?
    • Wie sieht der Op-Code aus?
    • Wie sehen typische C-Konstrukte im Opcode aus?

Register und Adressbereich des MSP430

alt:"Register des MSP430", src:"Datenblatt MSP430F2274, S. 6", w:40

alt: "Aufbau des Statusregisters", src: "Family Guide S. 46", w:75

alt: "Funktionsweise der Constant Generators", src: "Family Guide S. 46", w:75

alt:"Adressbereich des MSP430G2553", src:"Datenblatt MSP430G2553, S. 13", w:75

Speichereinheiten

  • Register des MSP430 sind 1 Wort / 2 Byte / 16 bit groß.
  • Aber Speicherzellen sind nur 1 Byte / 8 bit groß.
    → Es werden zwei Speicherzellen benötigt, um ein Register abzulegen.
  • In welcher Reihenfolge werden MSByte und LSByte im Speicher abgelegt?
    • Little Endian vs. Big Endian
    • Der MSP430 verwendet die Little Endian Reihenfolge
    • LSByte wird an der Adresse abgelegt
    • MSByte wird an der Adresse+1 abgelegt

Aufbau des MSP430-Maschinencodes

Übersicht von Matt Laubhan (University of Colorado at Colorado Springs): https://academics.uccs.edu/mlaubhan/MSP430/ece3430/MSP430InstructionSetEncodings.htm

Adressierungsarten

  • Konstanten (#0xA5, #8, ...)
  • Registeradressierung (R15, R14, PC, SR, SP, ...)
  • Absolute Adressierung (&0x200, &0x21, ...)
  • Indirekte Adressierung (@R14, nur Quelloperand)
  • Indirekte Adressierung mit Postinkrement (@R14+, nur Quelloperand)
  • Indizierte Adressierung (0x200(R14), 0(R15))

Rechenbefehle

  • Normale Rechenbefehle
    • MOV(.B) src, dst: dst = src;
    • ADD(.B) src, dst: dst += src;
    • ADDC(.B) src, dst: dst += src + C;
    • SUB(.B) src, dst: dst -= src;
    • SUBC(.B) src, dst: dst -= src + C;
    • CMP(.B) src, dst: dst - src; (nur Flags von Interesse, Ergebnis der Rechnung wird verworfen)
    • DADD(.B) src, dst: Dezimaladdition dst += src;
    • BIT(.B) src, dst: src & dst; (nur Flags von Interesse, Ergebnis der Rechnung wird verworfen)
    • BIC(.B) src, dst: dst &= ~src;
    • BIS(.B) src, dst: dst |= src;
    • XOR(.B) src, dst: dst ^= src;
    • AND(.B) src, dst: dst |= src;
    • RRA(.B) / RRC(.B) dst: dst = dst >> 1;, ggf. mit C
    • SWPB dst: Tauschen der Bytes
    • SXT dst: Vorzeichenerweiterung von Byte → Wort
  • Emulierte Rechenbefehle:
    • DEC(.B) dst: dst--; (SUB(.B) #1, dst)
    • DECD(.B) dst: dst -= 2; (SUB(.B) #2, dst)
    • INC(.B) dst: dst++; (ADD.x #1, dst)
    • INCD(.B) dst: dst += 2; (ADD.x #2, dst)
    • INV(.B) dst: dst = ~dst; (XOR.x #−1, dst)
    • RLA/C(.B) dst: dst = dst << 1; (ADD(C)(.B) dst, dst)
  • Operationen, wie DADD oder SWAP haben keine äquivalente Operationen in C. Aus diesem Grund gibt es in CCS Intrinsic:
    unsigned short   __bcd_add_short(unsigned short, unsigned short);
    unsigned long    __bcd_add_long(unsigned long, unsigned long);
    
    unsigned short   __swap_bytes(unsigned short a);
    

Verzweigungen im Assembler

  • Nutzen des CMP oder BIT-Befehls...
  • ...in Kombination mit Sprung-Befehlen:
    • JNE/JNZ Jump if not equal/zero
    • JEQ/JZ Jump if equal/zero
    • JNC/JLO Jump if no carry/lower
    • JC/JHS Jump if carry/higher or same
    • JN Jump if negative
    • JGE Jump if greater or equal
    • JL Jump if less
    • JMP Jump (unconditionally)
  • Beispiel einer einfachen if-Anweisung

Beispiel einer Verzweigung in C und Assembler

if (P1OUT > 0x0F) {
  P1OUT++;
} else {
  P1OUT--;
}

    CMP.B   #0x10, &0x21
    JHS     ($C$L1)
    DEC.B   &0x21
    JMP     ($C$L2)
$C$L1:
    INC.B   &0x21
$C$L2:
    ...

Schleifen in Assembler

  • Programmierung von Schleifen analog zu Verzweigungen: Nutzen von Sprungbefehlen
  • Vergleich: Aufzählen oder Abzählen in einer Schleife:

Beispiel einer aufzählenden Schleife von C und ASM

P1OUT = 0;
while (P1OUT < 0xAB) {
  P1OUT++;
}

    CLR.B   &0x21
    CMP.B   #0xAB,&0x21
    JHS     ($C$L4)
$C$L3:
    INC.B   &0x21
    CMP.B   #0xAB,&0x21
    JLO     ($C$L3)
$C$L4:
    ...

Beispiel einer abzählenden Schleife von C und ASM

P1OUT = 0xAB;
while (P1OUT > 0) {
  P1OUT--;
}

    CLR.B   &0x21
    TST.B   &0x21
    JEQ     ($C$L6)
$C$L5:
    DEC.B   &0x21
    JNZ     ($C$L5)
$C$L6:
    ...

Stapeloperationen

  • Stapel (engl. stack)
  • Mögliche Stapelzugriffe:
    • Ein Blatt von oben auf den Stapel legen. (PUSH)
    • Ein Blatt vom Stapel nehmen und lesen. (POP)
  • Der Stapel des MSP430 wird am Ende des RAM-Bereiches aufgebaut.
  • Der Stapel wird von hohen Adressen zu niedrigeren Adressen gefüllt.
  • Der Stapelzeiger zeigt auf das obereste Stapelwort.
  • PUSH: Wert des Stapelzeigers wird verringert.
  • POP: Wert des Stapelzeigers wird erhöht.

Funktionsaufruf

  • Eine Prozedur ist im Assembler eine Folge von Maschinenbefehlen, die einmalig definiert wird und an beliebigen Stellen im Programm aufgerufen werden kann.
  • Da der Aufruf an verschiedenen Stellen erfolgen kann, können keine einfachen Sprungbefehle verwendet werden, um die Prozedur aufzurufen.
  • Lösung: CALL- und RET-Befehl

Funktionsaufruf und Funktionsrumpf in C und Assembler

  // Funktionsaufruf
  func1();
  ...
}

void func1() {
  P1OUT = 0x5A;
}

    CALL    #func1
    ...

func1:
    MOV.B   #0x5a,&0x21
    RET

Funktionsaufruf und Funktionsrumpf mit einem Parameter in C und Assembler

  func2(0xA5);
  ...
}

void func2(uint8_t x) {
  P1OUT = x;
}

    MOV.B   #0xA5,R12
    CALL    #func2
    ...

func2:
    MOV.B   R12,&0x21
    RET
  • Für die ersten beiden Parameter werden die Register R14 und R12 verwendet.
  • Weitere Parameter werden auf dem Stack abgelegt.
  • Das Ergebnis einer Function wird in R12 abgelegt.
    Quelle: Texas Instruments, Mixing C and Assembler With the MSP430 (SLAA140)
  • Im Stack einer CPU kann nachvollzogen, welche Funktionen baumartig aufgerufen wurden:
    • z. B. kann im Fehlerfall der stack trace ausgegeben werden.
      def func1():
        x += 1
      
      def func2():
        func1()
      
      >> func2()
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
        File "<stdin>", line 2, in func2
        File "<stdin>", line 2, in func1
      UnboundLocalError: local variable 'x' referenced before assignment
      
    • Treten zu viele Funktionsaufrufe in einander auf (z. B. bei einem rekursiven Programm) kommt es zu einen stack overflow
      def func1():
        func1()
      
      >> func1()
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
        File "<stdin>", line 2, in func1
        File "<stdin>", line 2, in func1
        File "<stdin>", line 2, in func1
        [Previous line repeated 996 more times]
      RecursionError: maximum recursion depth exceeded
      

Interrupts

  • Tritt ein Interrupt auf dem MSP430 ein, so wird der aktuelle Befehl ausgeführt.
  • Das Statusregister und der PC wird auf den Stapel gelegt. (PUSH PC, PUSH SR)
  • Aus dem Interruptvektor wird die Startadresse der passenden ISR ausgelesen.
  • Die ISR wird ausgeführt.
  • Eine ISR endet mit einem RETI-Befehl. Dieser hat die Wirkung von (POP SR, POP PC)
  • Das Hauptprogramm arbeitet weiter.

Interrupt-Service-Routine in C und Assembler

#pragma vector=PORT2_VECTOR
__interrupt void PORT2_ISR() {
  if (P2IFG & BIT0) {
    P2IFG &= ~BIT0;
    __low_power_mode_off_on_exit();
  }
}

PORT2_ISR:
    BIT.B   #1,&0x2B
    JEQ     ($C$L8)
    BIC.B   #1,&0x2B
    BIC.W   #0x00f0, 0(SP)
$C$L8:
    RETI

Low-Power-Modi

  • Der LPM wird über die Bits SCG1/0 und OSCOFF/CPUOFF des Statusregisters bestimmt.

alt: "Aufbau des Statusregisters", src: "Family Guide S. 46", w:75

alt:"Low-Power-Modi",src: "Family-Guide S. 38ff", w:75

  • Betreten des Low-Power-Modus durch Setzen der Bits im SR
    __low_power_mode_4();  ->  BIS.W   #0x00f8,SR
    
  • Verlassen des LPM im Interrupt: Statusregister steht an oberster Stelle im Stapel
    __low_power_mode_off_on_exit();  ->  BIC.W   #0x00f0,0(SP)
    

Optimierungsmöglichkeiten

Automatische Optimierungsmöglichkeiten

  • Der C-Compiler für den MSP430 besitzt die Möglichkeit selbstständig den Code zu optimieren.
  • Siehe Project Properties/MSP430 Compiler/Optimization
  • Verschiedene Stufen der Optimierung:
    • off
    • 0: Register Optimizations
    • 1: Local Optimizations
    • 2: Global Optimizations
    • 3: Interprocedure Optimizations
    • 4: Whole Program Optimizations
  • Einstellung: Speed vs. size trade off
    Beispiel: Lookup-Table: Viel Speicher, aber extrem schnell
  • Problem: Je höher das Optimierungslevel, desto schlechter kann das Programm mit dem Live-Debugger untersucht werden (Funktionen und Variablen werden verändert)

Konstanten

Beispiel zu Konstanten und der #define-Anweisung

uint8_t keine_konstante = 0x77;
const uint8_t konstante = 0x55;
#define KONSTANTE 0x33

int main(void) {
  ...
  P1OUT = keine_konstante;
  P1OUT = konstante;
  P1OUT = KONSTANTE;
}

Disassembly des Beispiel oberhalb

 96     P1OUT = keine_konstante;
80d8:   42D2 0200 0021      MOV.B   &keine_konstante,&Port_1_2_P1OUT
 97     P1OUT = konstante;
80de:   40F2 0055 0021      MOV.B   #0x0055,&Port_1_2_P1OUT
 98     P1OUT = KONSTANTE;
80e4:   40F2 0033 0021      MOV.B   #0x0033,&Port_1_2_P1OUT

  • Wird eine Konstante nicht mit dem Schlüsselwort static versehen wird diese im RAM abgespeichert. (Die Variable keine_konstante hat die Adresse 0x200.)
  • Konstanten mit const oder #define werden gleich behandelt → die Konstante wird in diesen Fall direkt in die Anweisung eingetragen.
  • Unterschied zwischen #define und const
    • #define-Anweisungen werden in die Header-Datei geschrieben und sind für alle Module zugänglich.
    • #define-Anweisungen werden vorm Compilieren ausgewertet → vom Compiler können so Konstanten berechnet werden.
    • const-Konstanten haben einen festen Datentyp und sind daher sicherer als #define-Konstanten
    • consts kännen mit dem Schlüsselwort static versehen werden und als lokale Konstanten benutzt werden.
    • Um die Werte von #define-Konstanten sollte immer eine Klammer gesetzt werden. Beispiel:

Beispiel einer fehlerhaften #define-Anweisung

#define X 1+2
#define Y 2*X  // -> 2*1+2 = 4
...
#define X (1+2)
#define Y (2*X)  // -> (2*(1+2)) = 6

Praxis-Beispiele

Enum als Flags

typedef enum {
  ISR_WDT = (1 << 0),
  ISR_I2C_UPDATE = (1 << 1),
  ISR_BUTTON_PRESSED = (1 << 2),
  ISR_BATTERY_READY = (1 << 3),
  ISR_ALARM_BATTERY_LEVEL = (1 << 5),
  ISR_ALARM_WATCHDOG = (1 << 6),
  ISR_BUTTON2_PRESSED = (1 << 7)
} isr_flags_t;

  • Werden Enums automatisch nummeriert, so kann ein Element am Ende die Anzahl der Glieder angeben:

Automatisch nummeriertes Enum mit Längenangabe

typedef enum {
  ALARM_BATTERY_LEVEL = 0,
  ALARM_WATCHDOG,
  ALARM_KEY_PRESSED,
  ALARM_BATTERY,
  ALARM_LENGTH
} alarms_t;

  • Es gibt insgesamt vier Alarme → ALARM_LENTH bekommt automatisch den Wert 4 zugewiesen. ALARM_LENGTH kann z. B. als obere Grenze in einer for-Schleife verwendet werden.

Geschwindigkeitsmessung

Geschwindigkeitsmessung mit Timer A

void tick() {
  TACTL = TASSEL_2 + MC_2 + TACLR;
}

uint16_t tock() {
  TACTL = 0;
  return TAR - 12;
}

int main(void) {
  ...
  tick();
  t1 = tock(); // liefert 0
}

Funktionsaufrufe

  • Funktionsaufrufe benötigen mehr CPU-Laufzeit auf Grund der CALL- und RET-Anweisung.
  • Mit Hilfe von Makros können kleine Funktionsteile direkt im Code eingefügt werden.

Unterschied zwischen einer normalen Funktion und einem Makro

void func_normal() {
  P1OUT |= BIT0;
}

#define MACRO() (P1OUT |= BIT0)

int main(void) {
  ...
  tick();
  func_normal();
  t2 = tock();  // 12

  tick();
  MACRO();
  t3 = tock();  // 4
}

- In diesem Fall benötigt der normale Funktionsaufruf mehr Zeit und Speicherplatz.
- Der Compiler führt eine Optimierung erst ab 3-Interprocedure Optimizations durch.

Alternative: Inline-Funktionen
- Mit den Schlüsselwort inline kann der Compiler hingewiesen werden, den Funktionsaufruf zu unterbinden.

Beispiel einer Inline-Funktion

inline void func_normal() {
  P1OUT |= BIT0;
}

  • Inline-Funktionen besitzen keine Prototypen. Globale Inline-Funktionen müssen im Header definiert werden.

Praxis-Beispiel: Funktionen für die Pin-Konfiguration

Beispiel einer Header-Datei mit Funktionen für die Pin-Konfiguration
#ifndef PINS_H_
#define PINS_H_

#define P_CLOCK_FREQ_MHZ 1

// PORT 1
#define P_LED BIT0
#define P_SPIA_MISO BIT1
#define P_SPIA_MOSI BIT2
#define P_SPIRIT_CS BIT3
#define P_SPIA_SCK BIT4
#define P_UART_RXD P_SPIA_MISO
#define P_UART_TXD P_SPIA_MOSI

#define P_PORT1_UNUSED (BIT5 + BIT6 + BIT7)

// PORT2
#define P_SPIRIT_IRQ BIT0
#define P_XIN BIT6
#define P_XOUT BIT7
#define P_PORT2_UNUSED (BIT1 + BIT2 + BIT3 + BIT4 + BIT5)

// SPIA @ P1.1, P1.2, P1.4
#ifndef P_SPIA_DISABLE
inline void p_spia_setup() {
  P1SEL |= P_SPIA_MISO + P_SPIA_MOSI + P_SPIA_SCK;
  P1SEL2 |= P_SPIA_MISO + P_SPIA_MOSI + P_SPIA_SCK;
  P1DIR |= P_SPIA_MOSI + P_SPIA_SCK;
}
#endif

// LED @ P1.0 - OUT
#ifndef P_LED_DISABLE
inline void p_led_setup() {
  P1OUT &= ~P_LED;
  P1DIR |= P_LED;
}

inline void p_led_h() {
  P1OUT |= P_LED;
}

inline void p_led_l() {
  P1OUT &= ~P_LED;
}

inline void p_led_toggle() {
  P1OUT ^= P_LED;
}
#endif

...

// SPIRIT_IRQ @ P2.0 - interrupt falling edge
#ifndef P_SPIRIT_IRQ_DISABLE
inline void p_spirit_irq_setup() {
  P2DIR &= ~P_SPIRIT_IRQ;
  P2IES |= P_SPIRIT_IRQ;
  P2IFG &= ~P_SPIRIT_IRQ;
  P2IE |= P_SPIRIT_IRQ;
}

inline uint8_t p_spirit_irq_ifg() {
  return P2IFG & P_SPIRIT_IRQ;
}

inline void p_spirit_irq_ifg_clr() {
  P2IFG &= ~P_SPIRIT_IRQ;
}
#endif

// SETUP
inline void pins_setup() {
#if P_CLOCK_FREQ_MHZ == 1
  BCSCTL1 = CALBC1_1MHZ;
  DCOCTL = CALDCO_1MHZ;
#elif P_CLOCK_FREQ_MHZ == 8
  BCSCTL1 = CALBC1_8MHZ;
  DCOCTL = CALDCO_8MHZ;
#elif P_CLOCK_FREQ_MHZ == 12
  BCSCTL1 = CALBC1_12MHZ;
  DCOCTL = CALDCO_12MHZ;
#elif P_CLOCK_FREQ_MHZ == 16
  BCSCTL1 = CALBC1_16MHZ;
  DCOCTL = CALDCO_16MHZ;
#endif

  // PORT1
  p_led_setup();
  p_spirit_cs_setup();
  p_spia_setup();

  // PORT2
  p_spirit_irq_setup();

  // Unused Pins
  P1OUT &= ~P_PORT1_UNUSED;
  P1REN |= P_PORT1_UNUSED;
  P2OUT &= ~P_PORT2_UNUSED;
  P2REN |= P_PORT2_UNUSED;
}


#define p_delay_ms(t) (__delay_cycles(P_CLOCK_FREQ_MHZ * 1000L * (t)))
#define p_delay_us(t) (__delay_cycles(P_CLOCK_FREQ_MHZ * (t)))
  • Einfache Möglichkeit Pins zu tauschen, nachdem das gesamten Programm geschrieben wurde.
  • Keine Performance-Verluste durch inline-Funktionen.

SPI-Schnittstelle

  • Wird sehr häufig für den Transport von großen Datenmengen verwendet.
  • z. B. SPI-Slaves: Funkchips, Speicherchips (RAM, Flash, FRAM), SD-Karte
  • Große Datenmengen machen Timing von großer Bedeutung.
  • Theoretisch maximal erreichbare Datenrate = Frequenz der MCLK
    z. B. MCLK = 1 MHz → Datenrate = 1 Mbit/s = 125 kByte/s
  • Beispiel: Senden von Daten mit der Funktion spi_send(uint8_t *data, uint8_t len)

Optimierung einer SPI-Übertragung - Variante 1

void spi_send(const uint8_t *data, uint8_t len) {
  while (UCB0STAT & UCBUSY) {
  }

  for (uint8_t i = 0; i < len; i++) {
    UCB0TXBUF = data[i];
    while (UCB0STAT & UCBUSY) {
    }
  }
}

  • Laufzeit der Variante 1: 260 µs → 38,4 kByte/s
    alt:"Optimierung einer SPI-Übertragung - Signalverlauf Variante 1", w:66

Optimierung einer SPI-Übertragung - Variante 2

void spi_send(const uint8_t *data, uint8_t len) {
  while (UCB0STAT & UCBUSY) {
  }

  while (len > 0) {
    UCB0TXBUF = *data;
    data++;
    len--;
    while (UCB0STAT & UCBUSY) {
    }
  }
}

  • Laufzeit der Variante 2: 205 µs → 48,7 kByte/s
    alt:"Optimierung einer SPI-Übertragung - Signalverlauf Variante 2", w:66

Optimierung einer SPI-Übertragung - Variante 3

void spi_send(const uint8_t *data, uint8_t len) {
  while (len > 0) {
    while (UCB0STAT & UCBUSY) {
    }
    UCB0TXBUF = *data;
    data++;
    len--;
  }
}

  • Laufzeit der Variante 3: 198 µs → 50,5 kByte/s

Optimierung einer SPI-Übertragung - Variante 4

void spi_send(const uint8_t *data, uint8_t len) {
  while (len > 0) {
    while (!(IFG2 & UCA0TXIFG)) {
    }
    UCB0TXBUF = *data;
    data++;
    len--;
  }
}

  • Laufzeit der Variante 4: 142 µs → 70,4 kByte/s

Optimierung einer SPI-Übertragung - Variante 5

void spi_send(const uint8_t *data, uint8_t len) {
  while (len > 0) {
    UCB0TXBUF = *data;
    data++;
    len--;
  }
}

  • Laufzeit der Variante 5: 88 µs → 113,6 kByte/s
    alt:"Optimierung einer SPI-Übertragung - Signalverlauf Variante 5", w:66
  • Analyse des Verhalten der Funktion - Warum funktioniert die Funktion ohne Flag-Polling?
spi_send():
  TST.B   R13
  JEQ     ($C$L2)
$C$L1:
  MOV.B   @R12,&UCB0TXBUF
  INC.W   R12
  DEC.B   R13
  JNE     ($C$L1)
$C$L2:
  RET
Befehl Beschreibung Anzahl der CPU-Zyklen
MOV.B @R12,&UCB0TXBUF MOV, Indirekte Adressierung, Adressadressierung 5
INC.W R12 ADD #1, R12: Registeradressierung, Konstantengenerator 1
DEC.B R13 SUB #1, R13: Registeradressierung, Konstantengenerator 1
JNE ($C$L1) Bedingter Sprungbefehl 2
siehe Family Guide S. 60ff

→ 9 CPU-Zyklen pro SPI-Byte → Es entsteht eine Pause von 2 µs alle 2 Bytes.

  • Das Ganze funktioniert nur bei einen SPI-Taktteiler von 1 (UCB0BR0 = 1;)
  • Was passiert bei UCB0BR0 = 2;?

alt:"Fehlerhafte Datenübertragung bei geteilter SPI-Taktfrequenz", w:66

Angepasste Programmierung bei geteilter SPI-Taktfrequenz

void spi_send(const uint8_t *data, uint8_t len) {
  while (len > 0) {
    UCB0TXBUF = *data;
    __delay_cycles(7);
    data++;
    len--;
  }
}

  • 16 Taktzyklen werden von der SPI-Schnittstelle pro Byte benötigt, 9 Taktzyklen benötigt das Programm → Verzögerung um 7 Zyklen
  • __delay_cycles() verzögert das Programm um exakt die angegebenen Taktzyklen z. B. (NO-OP-Instruction)

__delay_cycles(7) Anweisung im Disassembly

8070:   3C00                JMP     (0x8072)
8072:   3C00                JMP     (0x8074)
8074:   3C00                JMP     (0x8076)
8076:   4303                NOP     

alt:"Angepasste Datenübertragung bei geteilter SPI-Taktfrequenz", w:66

Verwendung von Structs

Übergabe von Structs

  • Mit Hilfe von Structs können komplexe Datenstrukturen in C dargestellt werden.
  • Achtung! Stucts können sehr schnell groß werden und sollten daher wenn möglich nicht kopiert werden.
  • Die Übergabe eines Structs erfolgt im Assemblercode immer als Zeiger
  • Soll das Struct in der Funktion modifiziert werden, muss es als Zeiger übergeben werden.

Übergabe des Structs als Wert und als Zeiger

void struct_func1(example_t example) { ... }
void struct_func2(const example_t *example) { ... }
...
struct_func1(example);
struct_func2(&example);

63      struct_func1(example);
80d4:   410C                MOV.W   #0x8234,R12
80d6:   12B0 820C           CALL    #struct_func1
64      struct_func2(&example);
80da:   403C 8234           MOV.W   #0x8234,R12
80de:   12B0 8228           CALL    #struct_func2

Anordnung der Daten von Structs

  • Die Daten eine Structs werden hintereinander angeordnet.
  • Die Reihenfolge wird in der Regel eingehalten.
  • Es findet sogenanntes "Padding" statt. D. h. ein uint16_t darf nicht mit einer ungeraden Adresse beginnen. → Es muss ein leeres Byte eingefügt werden!
  • Am Beispiel:

Beispiel-Struct zur Veranschaulichung der Anordnung der Daten

typedef enum {
  GENDER_MALE = 1, GENDER_FEMALE = 2
} gender_t;

typedef struct {
  uint8_t age;
  gender_t gender;
  uint16_t size_mm;
  char name[8];
  uint32_t day_of_birth;
} person_t;

const person_t my_person = { .age = 23, .gender = GENDER_MALE, .size = 0x2233,
    .name = "Robert", .day_of_birth = 0xaabbccdd };

Beispiel-Struct im Memory Browser

0x823A my_person
0x823A 17  00  01  00  33  22  52  6F  62  65  72  74  00  00  DD  CC  BB  AA

  • Nach dem Feld age muss ein leeres Byte eingefügt werden.
  • Das Feld gender ist ein Enum und nimmt standardmäßig 16 Bit ein.
  • Die kann durch die Einstellung umgestellt werden: Project Properties → Build → MSP430 Compiler → Advanced Options → Runtime Model Options → Designate enum type → packed

Beispiel-Struct im Memory Browser mit 1 Byte Enums

0x823A my_person
0x823A 17  01  33  22  52  6F  62  65  72  74  00  00  DD  CC  BB  AA

Zugriff auf Struct-Felder

  • Der Zugriff auf Struct-Felder durch den MSP430 erfolgt wie bei herkömmlichen Variablen, wenn der Speicherplatz statisch festgelegt wurde.

Zugriff auf ein Feld eines statischen Structs

person_t ram_person;

int main(void) {
  ...
  ram_person.age++;
  ram_person.gender = GENDER_FEMALE;
}

53D2 0200           INC.B   &ram_person
43E2 0201           MOV.B   #2,&0x0201

Versenden und Empfangen von Structs über Kommunikationskanäle

  • Zum Versenden muss ein Struct umgewandelt werden in eine Folge von Bytes (Char-Array).
  • Dies erfolgt in C mit einen sogennanten Type-Cast:

Beispiel: Versenden eines Structs über SPI

spi_send((uint8_t *) &my_person, sizeof(my_person));

  • Entsprechend können die Daten beim Empfänger wieder in ein Struct umgewandelt werden. Hierfür kann ebenfalls ein Type-Cast verwendet werden.
  • Es können sogar auf einzelne Bytes des Structs zugriffen werden: ((uint8_t*) &my_person)[index]
  • Praxis-Beispiel: Aufbau einer Register-Map eines SPI- oder I²C-Slaves:
typedef struct {
  uint8_t status;
  uint8_t button_duration;
  uint8_t led_output;
  uint8_t beeper;
  uint16_t battery_adc_value;
  uint32_t time;
} i2cs_data_t;