Passer du 8-bits au 32-bits - Initialisation du XMC1100

Posté le par
Tags
:

Dans ce billet je vais tenter de décrire l’initialisation du processeur présent dans le kit de déveloeppemnt XMC 2 Go. Dans un premier temps je vais m’efforcer de décrire quelques concepts généraux. Dans un deuxième temps, je présenterais le code que j’utilise. Il va sans dire que ce code est certainement perfectible.

Concepts généraux

Le mode d’exécution

Les processeurs modernes peuvent fonctionner dans plusieurs modes d’exécution. Par exemple, les processeurs dont l’ISA est ARM comme le Cortex-A8 peuvent fonctionner dans 8 modes différents. Ces modes peuvent être privilégiés ou non. Certains sont utilisés lors de l’exécution du déclenchement d’une interruption (FIQ, IRQ) d’autres a la suite d’une instruction particulière (Supervisor).

L’avantage de l’utilisation de ces modes et l’utilisation de la commutation de banque (en Anglais bank switching), où une banque est un ensemble de registres. Ainsi il existe une banque pour chaque mode, et ces banques sont automatiquement changées lors de la commutation de mode. Si vous souhaitez utiliser le registre R1 dans votre gestionnaire d’interruption, vous n’avez alors pas besoin de le sauvegarder à l’entrée du gestionnaire, puis de le restaurer à la sortie.

Sur les cortex-M0 dont l’ISA est Thumb et donc sur le XMC1100, seuls deux modes sont disponibles. Le mode Thread est le mode d’exécution par défaut du processeur. Il peut utiliser comme pointeur de pile SP soit le pointeur dit principal MSP soit le pointeur de pile pour processus utilisateur PSP. Le mode Handler est le mode utilisé lors du déclenchement d’une interruption et il utilise quant à lui toujours le MSP. J’utiliserais pour ma part quasi exclusivement le mode Thread avec le PSP. Notre microcontrôleur étant de petite taille, un seul registre est véritablement banqué, et ce registre est comme vous l’avez surement deviné le pointeur de pile SP.

Lors de l’arrivé d’une interruption, et si le processeur est en mode Thread, les registres R1-R3, R12, LR, PC et xPSR sont empilés sur la pile courante, puis la commutation de banque suivit de l’exécution du gestionnaire d’interruption sont réalisé. Les registres précédemment cités comme sauvegardés sur la pile peuvent être considéré comme banqué du point de vue de l’écriture du gestionnaire d’interruption, mais il faut garder à l’esprit qu’ils sont en réalité empilés lors de la détermination de la taille de votre pile.

La table de vecteurs

La table de vecteurs n’est pas une notion mathématique, inutile de partir en courant ! Un vecteur d’interruption, puisque c’est de cela qu’il s’agit, est un moyen pour accéder au gestionnaire d’interruption et peut prendre plusieurs formes. L’ensemble des vecteurs d’interruptions est la table de vecteurs. Il existe plusieurs méthodes pour implémenter cette table de vecteurs.

La méthode Fetch consiste à remplir la table de vecteurs avec les adresses respectives de chacun des gestionnaires d’interruptions.
La méthode Predefined consiste à remplir la table de vecteurs avec les gestionnaires d’interruptions eux-mêmes. Cependant, la taille allouée à ceux-ci est généralement relativement limitée, et les vecteurs d’interruptions se limitent en général à un saut vers le véritable gestionnaire.

Sur les cortex-M0, la table de vecteurs se présente comme suite :

Addresse pour le XMC1100 Table des vecteurs
0x10001000 Pointeur de pile de l’application
0x10001004 Addresse du gestionnaire de reset
0x10001008 Réservé
….

On voit ici que le début de la table de vecteurs permet de définir l’état initial du processeur juste avant de démarrer le logiciel. Cette portion de la table se situe pour le XMC1100 au début de la flash.

Il existe une autre table de vecteur situé en ROM qui définit les adresses des gestionnaires d’interruptions. Cette table est de type Fetch, mais elle ne peut pas être modifiée. Chacune des adresses pointent vers une adresse en RAM. C’est à ces adresses que nous pouvons écrire une troisième table de vecteurs de type Predefined.

Exemple de code

Voici un exemple du code que j’utilise.

Script de liaison

Le script de liaison a été largement inspiré de celui proposé par Frank Duignan.

On commence par définir quelques constantes que l’on utilisera par la suite.


/* useful reference: www.linuxselfhelp.com/gnu/ld/html_chapter/ld_toc.html */
/* Written by Frank Duignan */

HEAP_SIZE           = 2048;
THREAD_STACK_SIZE   = 2048;
MAIN_STACK_SIZE     = 128;

On définit ensuite les zones mémoires. Ces informations peuvent être trouvées dans la datasheet de votre microcontrôleur.


MEMORY
{
    flash : org = 0x10001000, len = 64k
    ram : org = 0x20000000, len = 16k
}

On commence à définir la position des sections dans la mémoire. On écrit les sections .vectors et .text dans la flash.


SECTIONS
{

  . = ORIGIN(flash);
  .text : {          
    *(.vectors); /* The interrupt vectors */
    *(.text);
  } >flash

Puis on définie les sections .remapped_vectors et .data en RAM. Cette section est un peu particulière, car son adresse de relocation et son adresse de chargement sont différentes. Son adresse de de relocation est quelque part en RAM >ram, tandis que l’adresse de chargement est en flash AT>flash. On définit d’ailleurs un symbole contenant l’adresse de chargement data_flash = LOADADDR(.data); et un symbole pour l’adresse de relocation data_ram = .;.


  . = ORIGIN(ram);
  .data : {      
    __data_flash = LOADADDR(.data);
    __data_ram = .;
    *(.remapped_vectors);
    *(.data);
    __data_ram_end = .;
  } >ram AT>flash
  __data_size = SIZEOF(.data);

On définie ensuite les sections contenant nos variables, c’est a dire .bss pour les variables globales, et .heap (le tas) pour l’espace alloué par les fonctions de type malloc.


  __bss = .;
  .bss : {
    *(.bss);
  } > ram
  __bss_end = .;
  __bss_size = SIZEOF(.bss);

  __heap = .;
  .heap : {
    . = ALIGN (4);
    . += HEAP_SIZE;
  } > ram
  __heap_end = .;
  __heap_size = SIZEOF(.heap);

Enfin on positionne notre pile, qui contiendra entres autres les variables locales.


  __stack = .;
  .stack : {
    . = ALIGN (4);

    . += THREAD_STACK_SIZE;
    . = ALIGN (4);
    THREAD_STACK_TOP = . ;

    . += MAIN_STACK_SIZE;
    . = ALIGN (4);
    MAIN_STACK_TOP = . ;
  } > ram
  __stack_end = .;
  __stack_size = SIZEOF(.stack);

}

Initialisation en Assembleur

Maintenant que nous avons un script de liaison positionnant certaines sections en mémoire, nous allons pouvoir écrire du code assembleur pour initialiser notre processeur.

Dans un premier temps on définit quelques constantes utiles.


.cpu cortex-m0
.equ  THREAD_MODE_MPS, 0x0
.equ  THREAD_MODE_PSP, 0x2

Puis on définie la table de vecteurs utilisée pour l’initialisation du processeur.


.text
.code    16
.section    .vectors
@ .section    .vectors,"aw",%progbits
.align    2
.word    MAIN_STACK_TOP 
.word    init 
.word    0 
.word    0
.word    -1
.word    -1

On remplit aussi la table de vecteurs contenant les sauts vers les gestionnaires d’interruptions. Si aucun gestionnaire n’est défini, on remplit de 0, sinon on charge l’adresse du gestionnaire dans R0, puis on copie cette valeur dans PC. Pensez bien a définir le RESET. On a ici également défini quatre autres gestionnaires.


@ This section is loaded in Flash, but the relocation is done in RAM at 0x20000000
.section    .remapped_vectors, "ax"
@.section    .remapped_vectors,"ax",%progbits
.code 16
.align 2
.type _vector, function
_vectors:
.long 0
@ Reset
ldr r0, =init
mov pc, r0
.long 0
@ Hard Fault
ldr r0, =handler_hardfault
mov pc,r0     
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
@ SVCall 
ldr r0,=handler_svc
mov pc,r0
.long 0 
.long 0 
@ PendSV
ldr r0, =handler_psv
mov pc, r0
@ SysTick
ldr r0, =scheduler
mov pc, r0
.long 0                 @ IRQ0
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 
.long 0 

On peut enfin commencer à écrire le code d’initialisation. On copie ici la section .data(souvenez-vous de la section ayant une adresse de relocation différente de celle de chargement!) depuis la flash vers la ram.


.text
.code    16
.align    2
.global    startup
.func     startup
init:
  @ Copy Data from flash to his relocation address
  LDR     r1, =(__data_flash)  @ Src register
  LDR     r3, =(__data_ram)  @ Dst register
  LDR     r5, =(__data_size)               @ Counter
vector_loop:
  LDR     r2,[r1, #0]
  STR     r2,[r3, #0]
  ADD     r1, r1, #4
  ADD     r3, r3, #4
  SUB     r5, #1
  BNE     vector_loop 

On remplie la section .bss avec des zéros.


  @ Initialise BSS
  LDR     r3, =(__bss)
  LDR     r5, =(__bss_size)
  MOV     r2, #0
bss_loop:
  STRB    r2,[r3, #0]
  ADD     r3, r3, #1
  SUB     r5, #1
  BNE     bss_loop

Pour faciliter le débogage, on remplit également la pile de 0.


  @ Initialise Stack
  LDR     r3, =(__stack)
  LDR     r5, =(__stack_size)
  LDR     r2, =0x00
stack_loop:
  STRB     r2,[r3, #0]
  ADD     r3, r3, #1
  SUB     r5, #1
  BNE     stack_loop

On configure le processeur de manière à ce qu’il utilise le registre PSP comme pointeur de pile.


  @ Switch to Thread mode with PSP
  MOV     r3,#(THREAD_MODE_PSP)
  MSR     CONTROL,r3
  LDR     r4,=THREAD_STACK_TOP
  MOV     sp,r4
  ISB

Maintenant que la bss et la pile sont utilisables, on peut enfin commencer à utiliser du code C sans prendre de risque.


  BL       _main
  .size   startup, . - startup
  .endfunc

La fonction _main

Voici le code minimal pour la fonction _main.


void _main()
{
  for(;;) {
  }
}

Avouez que même si notre code C ne fait rien, tout cela est plutôt classe!

Le petit supplément qui clignote

Encore une fois en pompant joyeusement le fichier d’en-tête de Frank Duignan on peut même réussir à faire clignoter une LED.


#include "xmc1100.h"

static void _delay(unsigned len)
{
  while(len--);
}

void _main()
{
  for(;;) {
    _delay(5000);
    P1_OUT ^= 0x1;
  }
}

Passer du 8-bits au 32-bits - Découverte du XMC 2 Go

Posté le par
Tags
:

En attendant la réception de mon XMC 2 Go, j’ai commencé à lire un peu. Ce kit est donc équipé des composants suivants:

  • un microcontrôleur XMC1100
  • 2 LEDs
  • un debugger J-link
  • 2x8 broches d’extension

Le XMC1100 est un microcontrôleur Cortex-M0 fonctionnant à 32 MHz et disposant de 64 kB de flash et 16 kB de RAM.

Je vous propose une petite comparaison avec mon Arduino Duemilanove, comparaison sans prétention d’exhaustivité ni de justesse. Je n’ai à l’heure de l’écriture fait que lire les datasheets.

Paramètres \ Kit Arduino Duemilanove XMC 2 Go
Architecture AVR ARM
Fréquence d’horloge 16 MHz 32 MHz
Mémoire Flash 32KB 64KB
Mémoire RAM 2KB 16KB

Si l’on regarde les périphériques disponibles, l’Arduino dispose de trois interfaces série (UART, SPI et I2C), tandis que le XMC 2 Go ne dispose que de deux interfaces. Cependant, le XMC est a priori plus souple puisque chacune de ses interfaces peut être configurée comme UART, SPI, I2C, I2S ou LIN. Ce point reste à vérifier, car même si le microcontrôleur le permet bien, la conception du kit ne le permet pas forcément.

Alors que l’ADC de l’Arduino est un ADC 10-bit réalisant un échantillonnage à 77 K échantillons par seconde, l’ADC du XMC permet d’obtenir un échantillonnage de 10-bit à presque 0.97 M échantillons par seconde, et un échantillonnage de 10-bit a presque 1.03 M échantillons par seconde. D’autre part, l’Arduino n’offre que 6 canaux, tandis que le XMC en offre 12.

Enfin, l’Arduino offre 4 PWM 8-bits et 2 PWM 16-bits, tandis que le XMC propose 4 PWM 16-bits. Petit point à noter, l’Arduino utilise 2 compteurs 8-bits et un compteur 16-bits, tandis que le XMC utilise 4 compteurs 16-bits. Cela pourrait éventuellement impacter la flexibilité de l’Arduino.

Passer du 8-bits au 32-bits - Le choix de la plateforme

Posté le par
Tags
:

Développeur depuis déjà quelques années déjà sur microcontrôleur 8-bits (principalement Atmel, avec où sans les bibliothèques Arduino), j’ai récemment fait l’acquisition d’un nouveau jouet, le XMC 2 Go. Cette petite carte proposée par Infineon est de la taille d’une clé USB et embarque un microcontrôleur Infineon XMC1100.

Le choix s’est dès le début orienté vers un petit microcontrôleur ARM pour des raisons évidentes de coût. Un petit tour sur le site internet de votre revendeur vous en convaincra, les plateformes de développement pour microcontrôleur ARM sont parmi les moins chères, moins chères même que les kits de développement pour microcontrôleurs 8 ou 16-bits, et le Cortex-M0 est certainement en bas de la gamme de prix. Pour pas si chère que cela, on trouve également des Cortex-M3 ou Cortex-M4.

Dans le tableau suivant, je donne un petit récapitulatif de la gamme de processeurs ARM. Une Description détaillée peut être trouvée sur le site de ARM.

Famille Utilisation Jeu d’instruction disponible
ARM Cortex-A Microprocesseur ARM ou Thumb
ARM Cortex-R Microprocesseur temps réel ARM ou Thumb
ARM Cortex-M Microcontrôleur Thumb

Les bricoleurs qui viennent du monde du 8-bits sont plutôt concernés par les
Cortex-M. Pour y voir plus clair dans la gamme de processeurs ARM, vous pouvez
également jouer avec le Sélecteur de processeurs
ARM
.

Une fois que vous y verrez plus clair, vous pourrez choisir votre plateforme de développement. Ayant travaillé chez Infineon j’ai fait le choix peu objectif d’acheter la XMC 2 Go. La gamme de produits ST (Discovery et Nucleo) est également sympathique et a l’énorme avantage de disposer d’une connectique plus fournie et de bouton poussoirs directement intégré.

Lors de l’achat, faites tout de même très attention à la présence d’un débugger matériel comme c’est le cas sur la XMC 2 Go d’Infineon où sur les Discoveries et les Nucleos de ST.