Speicher beim STM32 (Cortex-M3/Cortex-M0) sparen

In der letzten Zeit programmiere ich recht viel mit dem STM32. Dabei handelt es sich um einen Microcontroller mit ARM-v7 Kern (Cortex-M0, Cortex-M3, Cortex-M4), der sich grob im Preisniveau von AVRs bewegt, aber deutlich performanter ist. Seitens des Herstellers (ST Microelectronics) wird eine angenehm nutzbare Firmware-Library (Standard Peripherals Library) mitgeliefert, sodass man eigentlich direkt starten kann. Achsoo, GCC gibts natürlich auch als Compiler: . Das ganze wird dann unter Eclipse programmiert und mit OpenOCD debuggt.

Jetzt aber zum eigentlichen Thema. Wenn man nämlich wie von ST in den Beispielen der Firmware-Library die Peripherie initialisiert, dann sieht das in etwa so aus:

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOC, ENABLE);

GPIO_InitTypeDef gpio;
gpio.GPIO_Mode = GPIO_Mode_OUT;
gpio.GPIO_OType = GPIO_OType_PP;
gpio.GPIO_PuPd = GPIO_PuPd_NOPULL;
gpio.GPIO_Speed = GPIO_Speed_Level_1;
gpio.GPIO_Pin = GPIO_Pin_14 | GPIO_Pin_15;
GPIO_Init(GPIOC, &gpio);

Zum einen ist dieser Code recht unübersichtlich, zum anderen passiert aber hier etwas, was man eigentlich gar nicht möchte. Zuerst wird nämlich auf dem Stack Speicher für die Struktur reserviert, dann im Programm Feld für Feld mit Werten gefüllt und danach an die Init-Funktion übergeben. Im Assembler sieht das übrigens so aus:

  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOC, ENABLE);
 800899a:	2380      	movs	r3, #128	; 0x80
 800899c:	031b      	lsls	r3, r3, #12
 800899e:	1c18      	adds	r0, r3, #0
 80089a0:	2101      	movs	r1, #1
 80089a2:	f7fa f919 	bl	8002bd8 <RCC_AHBPeriphClockCmd>

  GPIO_InitTypeDef gpio;
  gpio.GPIO_Mode = GPIO_Mode_OUT;
 80089a6:	1c3b      	adds	r3, r7, #0
 80089a8:	2201      	movs	r2, #1
 80089aa:	711a      	strb	r2, [r3, #4]
  gpio.GPIO_OType = GPIO_OType_PP;
 80089ac:	1c3b      	adds	r3, r7, #0
 80089ae:	2200      	movs	r2, #0
 80089b0:	719a      	strb	r2, [r3, #6]
  gpio.GPIO_PuPd = GPIO_PuPd_NOPULL;
 80089b2:	1c3b      	adds	r3, r7, #0
 80089b4:	2200      	movs	r2, #0
 80089b6:	71da      	strb	r2, [r3, #7]
  gpio.GPIO_Speed = GPIO_Speed_Level_1;
 80089b8:	1c3b      	adds	r3, r7, #0
 80089ba:	2200      	movs	r2, #0
 80089bc:	715a      	strb	r2, [r3, #5]
  gpio.GPIO_Pin = GPIO_Pin_14 | GPIO_Pin_15;
 80089be:	1c3b      	adds	r3, r7, #0
 80089c0:	22c0      	movs	r2, #192	; 0xc0
 80089c2:	0212      	lsls	r2, r2, #8
 80089c4:	601a      	str	r2, [r3, #0]
  GPIO_Init(GPIOC, &gpio);
 80089c6:	4a04      	ldr	r2, [pc, #16]	; (80089d8 <DIR_Config+0x44>)
 80089c8:	1c3b      	adds	r3, r7, #0
 80089ca:	1c10      	adds	r0, r2, #0
 80089cc:	1c19      	adds	r1, r3, #0
 80089ce:	f7f9 fddb 	bl	8002588 <GPIO_Init>

Ziemlich aufwendig für ein wenig Initialisierung, die sich nie ändert, gell? In Summe sind das 56 Bytes Programmcode.

Wenn man nun geschickt C99-Struct-Initialisierungen mit ein paar Speichermodifiern kombiniert, so erhält man folgenden Code, der effektiv das Gleiche macht:

  /* GPIOC Periph clock enable */
  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOC, ENABLE);

  const GPIO_InitTypeDef gpio = {
      .GPIO_Mode = GPIO_Mode_OUT,
      .GPIO_OType = GPIO_OType_PP,
      .GPIO_PuPd = GPIO_PuPd_NOPULL,
      .GPIO_Speed = GPIO_Speed_Level_1,
      .GPIO_Pin = GPIO_Pin_14 | GPIO_Pin_15
  };

  GPIO_Init(GPIOC, &gpio);

Zusätzlich kann man noch die Funktionssignatur der Funktion GPIO_Init ändern, sodass man einen Pointer übergibt, an dem sich weder die Adresse noch der Inhalt ändert:

void GPIO_Init(GPIO_TypeDef* GPIOx, const GPIO_InitTypeDef* const GPIO_InitStruct);

Damit werden die Daten nicht erst im RAM aufbereitet und dann an die GPIO_Init übergeben sondern die Funktion bekommt direkt die Flash-Adresse der Daten übergeben. Das Ergebnis sieht erstaunlich simpel im Assembler-Code aus:

  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOC, ENABLE);
 800899a:	2380      	movs	r3, #128	; 0x80
 800899c:	031b      	lsls	r3, r3, #12
 800899e:	1c18      	adds	r0, r3, #0
 80089a0:	2101      	movs	r1, #1
 80089a2:	f7fa f919 	bl	8002bd8 <RCC_AHBPeriphClockCmd>
  const GPIO_InitTypeDef gpio = {...}
 80089a6:	1c3b      	adds	r3, r7, #0
 80089a8:	4a05      	ldr	r2, [pc, #20]	; (80089c0 <DIR_Config+0x2c>)
 80089aa:	ca03      	ldmia	r2!, {r0, r1}
 80089ac:	c303      	stmia	r3!, {r0, r1}
  GPIO_Init(GPIOC, &gpio);
 80089ae:	4a05      	ldr	r2, [pc, #20]	; (80089c4 <DIR_Config+0x30>)
 80089b0:	1c3b      	adds	r3, r7, #0
 80089b2:	1c10      	adds	r0, r2, #0
 80089b4:	1c19      	adds	r1, r3, #0
 80089b6:	f7f9 fde7 	bl	8002588 <GPIO_Init>

Macht in Summe 32 Bytes Programmcode. Also haben wir uns einen Haufen Anweisungen, Stack-Speicher in Größe der Struktur und 24 Bytes (43%) Code gespart. Das mag in erster Instanz lächerlich wirken, jedoch wird bei der Initialisierung mehr als ein Pin initialisiert. Prinzipiell lässt sich dieser Trick nämlich auch bei DMA-, ADC-, TIMer-, DAC-, …. -Initialisierungen verwenden.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert