Logiciels Libres et Systèmes Embarqués


5.5. Configuration du processeur

Le point d'entrée du noyau Linux pour PowerPC 405 est la fonction _start. Pour que le noyau puisse récupérer les paramètres que U-Boot lui a passés, il faut qu'il connaisse la structure bd_t. Il faut donc rajouter les lignes suivantes dans le fichier arch/ppc/platforms/virtex.h :

#ifndef __VIRTEX_H__
#define __VIRTEX_H__

#include "asm/ppcboot.h" /* struct bd_t */
#define bi_tbfreq bi_intfreq
#endif /* __VIRTEX_H__ */

On doit aussi définir bi_tbfreq car la fonction du noyau qui recherche la fréquence du processeur utilise ce champ à la place de bi_intfreq. On aurait très bien pu modifier la fonction en question, mais il est préférable d'avoir un minimum de modifications à faire dans le noyau lui-même, afin de limiter l'intrusion.

5.5.1. Initialisation temporaire de la MMU

La "Memory Management Unit" (MMU), permettant la protection et la translation d'adresses, est ensuite configurée dans la fonction initial_mmu de manière à ce que le noyau puisse effectuer les premières initialisations. Pour cela, les trente-deux premiers méga-octets de la mémoire sont maintenant accessibles à l'adresse 0xC0000000 ; deux entrées dans le "Translation Look-aside Buffer" (TLB) de seize méga-octets sont donc utilisées. Le TLB est une sorte de cache pour la correspondance des pages mais dont la politique de remplacement est réalisée par le logiciel.

Cette translation est nécessaire car le segment de code est mappé à cette adresse. Toujours dans cette même fonction, le registre de base de la table des vecteurs d'exceptions est mise à jour avec la valeur 0x00000000. Il faut savoir que les gestionnaires d'exceptions (disponibles dans ce même fichier) ne fonctionnent qu'avec des adresses physiques, et non pas avec des adresses virtuelles gérées par la MMU et cela pour éviter les défauts de TLB.

5.5.2. Adaptation au processeur

La MMU est activée en effectuant un branchement à start_here (située à une adresse 0xC000xxxx). Dans un premier temps, cette fonction initialise les registres permettant d'identifier le thread courant du noyau, ainsi que le registre de pointeur de pile. Puis elle effectue un appel à la fonction early_init. Tout d'abord, cette dernière met à zéro le contenu du segment BSS (données non-initialisées). Puis elle fait appel à la fonction identify_cpu qui permet d'identifier le processeur disponible, notamment pour connaître quelles sont les fonctionnalités disponibles, ainsi que les bogues à contourner.

La recherche du modèle du processeur s'effectue par itérations sur les entrées de la table cpu_specs. La concordance s'effectue grâce au registre spécial "Processeur Version Register" (PVR), il faut donc rajouter une entrée dans cette table (fichier arch/ppc/kernel/cputable.c) pour que le processeur soit correctement identifié :

+ {
+   0xffffe000,     // masque du PVR
+   0x20010000,     // valeur du PVR
+   "405 Virtex",    // nom du processeur
+   // fonctionnalités noyau :
+   CPU_FTR_SPLIT_ID_CACHE | CPU_FTR_USE_TB | CPU_FTR_CAN_DOZE,
+   // fonctionnalités utilisateur :
+   PPC_FEATURE_32 | PPC_FEATURE_HAS_MMU,
+   32, // taille du cache d'instructions
+   32, // taille du cache de données
+   0,  // pointeur sur la fonction de configuration du processeur
+ },
  #endif /* CONFIG_40x */

L'itération sur la table s'arrête lorsqu'on obtient « PVR & PVR_Mask == PVR_Value ».

Afin de connaître quelles sont les valeurs exactes à mettre dans les champs de ce tableau, il faut se reporter au document "PowerPC Processor Reference Guide" fourni par Xilinx. Le noyau fait ensuite appel à la fonction do_cpu_ftr_fixups qui permet de modifier à la volée le code du noyau, selon les fonctionnalités présentes dans le processeur. Cette fonction est courte (moins de 40 instructions) mais très technique. Heureusement qu'elle est très bien expliquée dans le fichier Documentation/powerpc/cpu_features.txt.

5.5.3. Initialisation de la plateforme

Le noyau continue alors avec l'initialisation de la machine, grâce à l'appel de la fonction machine_init. Dans cette dernière, la ligne de commande par défaut, donnée lors de la configuration du noyau, est copiée dans la variable cmd_line, elle sera éventuellement remplacée par la ligne de commande passée en argument. Le noyau appelle ensuite la fonction platform_init, cette dernière possède en tant qu'arguments les paramètres passés au noyau lors du chargement. Dans un premier temps, cette fonction cherche les informations sur la taille de mémoire disponible, la ligne de commande, les adresses du RAMDisk, le pointeur sur la structure d'informations, ... Elle essaye de récupérer ces informations grâce au système de "boot info" utilisé par certaines cartes. Or notre carte FPGA utilise un autre système équivalent (structure bd_t), ce passage est donc inutile.

Il faut, cependant, modifier la fonction find_bootinfo, en remplaçant la définition de extern char __bss_start[] par extern char __bss_start, et les utilisations de __bss_start par &__bss_start. En effet, certaines versions de GCC n'acceptent pas que __bss_start soit défini avec des types différents. Puisque cette variable a déjà été définie en tant que char (et non pas en tant que char[]) dans la fonction early_init, cela implique alors de changer dans find_bootinfo les utilisations de la variable __bss_start.

Arrive ensuite le système utilisé par notre carte, c'est à dire la fameuse structure bd_t. La fonction récupère donc les paramètres passés au chargement pour obtenir les informations précédentes. Ensuite, elle initialise les pointeurs sur les fonctions propres à notre architecture, comme par exemple les fonctions relatives au processeur, aux IRQ ou aux entrées/sorties. Enfin, elle appelle la fonction board_init pour initialiser notre carte. Dans notre cas, elle ne fait rien d'autre que d'initialiser le pointeur de la fonction du redémarrage de la carte :

#include "virtex.h"
#include "linux/init.h"  /* __init */
#include "asm/machdep.h" /* ppc_md */
#include "asm/processor.h" /* mtspr */

static void board_restart(char *cmd)
{
    mtspr(SPRN_DBCR0, DBCR0_RST);
}

void __init board_init(void)
{
    ppc_md.restart = board_restart;
}

La fonction board_restart est très simple, elle place dans le registre DBCR0 les bits signifiants que l'on souhaite redémarrer le système matériel.

La macro __init précise qu'il s'agit d'une fonction d'initialisation, et que par conséquent la mémoire occupée par cette fonction pourra être libérée après le démarrage du noyau. On peut voir le résultat de ce phénomène lors du démarrage grâce au message suivant :

Freeing unused kernel memory: 36k init

5.5.4. Initialisation définitive de la MMU

L'initialisation définitive de la MMU se fait par la fonction MMU_init. Dans un premier temps, elle fait appel à MMU_setup afin rechercher des paramètres propres à la taille de la mémoire qui auraient été passés en paramètres (mem=32M par exemple). Cette fonction recherche ensuite ces mêmes informations dans la structure de données remplie par le chargeur. En comparant les résultats obtenus, MMU_init configure le noyau avec le choix qui parait le plus cohérent.

Le noyau continue ensuite avec l'initialisation de la MMU dépendante du processeur, grâce à la fonction MMU_init_hw. Il faut savoir que le registre "Zone Protection Register" (ZPR) permet aussi d'avoir un système de protection simple sans pour autant utiliser la TLB (c'est à dire la MMU), cela est utile lors que l'on repasse en mode réel (par exemple dans les gestionnaires d'interruptions). La fonction utilise donc ce registre ZPR pour définir deux zones de protections différentes, l'une pour le noyau et l'autre pour les applications utilisateurs. Ensuite, MMU_init_hw vide le cache d'instructions afin de le laisser dans un état connu et cohérent, puis le configure pour qu'il fonctionne en mode réel lorsque le noyau se trouve dans les gestionnaires d'interruptions.

Grâce à la fonction mapin_ram, toute la mémoire vive est mappée (avec les bons paramètres d'accès) à l'adresse 0xC0000000. On pourrait alors se demander pourquoi le code du noyau se situe à cette adresse. Comme nous l'avons vu, la RAM doit être accessible à l'adresse 0x00000000 puisque l'initialisation du noyau est relative à cette adresse lorsque la gestion de la mémoire virtuelle n'est pas activée. Traditionnellement, dans les systèmes embarqués à base de PowerPC, la RAM est bien à l'adresse (physique) 0x00000000, et les registres d'entrées/sorties sont au-delà de l'adresse (physique) 0x80000000. Or, le noyau Linux réserve l'espace d'adressage 0x00000000-0x7FFFFFFF pour les processus utilisateurs. Donc, étant donné que le noyau peut avoir besoin de ces périphériques avant que le gestionnaire de mémoire virtuel ne soit complètement configuré, les développeurs ont choisi d'exécuter le noyau à l'adresse (virtuelle) 0xC0000000 afin de ne pas créer d'interférences.

Dans la suite de la fonction MMU_init, l'adresse de base où seront accessibles les périphériques en mode virtuel est définie à 0xFE000000. Puis le noyau enregistre les plages d'entrées/sorties utilisées par les périphériques disponibles sur la carte. Cela est effectué dans la fonction m4xx_map_io. Cette fonction a été écrite pour le 405GP, il faut donc ignorer les spécificités de ce "System-On-Chip" :

+ #ifndef CONFIG_VIRTEX
      io_block_mapping(PPC4xx_ONB_IO_VADDR,
                       PPC4xx_ONB_IO_PADDR, PPC4xx_ONB_IO_SIZE, _PAGE_IO);
+ #endif

Le noyau Linux continue avec l'exécution de board_io_mapping, qui a pour but de déclarer les plages d'entrées/sorties utilisées par les périphériques. Cependant, il est préférable de déclarer ces plages d'adresses dans les pilotes correspondants. La fonction board_io_mapping est donc vide :

void __init board_io_mapping(void)
{
    return;
}

A la fin de la fonction MMU_init, l'initialisation du gestionnaire de contextes est effectuée grâce à l'appel de la fonction mmu_context_init. Finalement, de retour à la fonction start_here, le noyau repasse en mode réel afin de vider les entrées de la MMU nouvellement configurée. Le passage au mode virtuel se fait par un branchement à la fonction start_kernel.

Le tableau suivant donne un récapitulatif de l'adressage dans les différents modes d'accès à la mémoire :

Tableau 5.4. Adressage dans les différents modes d'accès à la mémoire

adressemode réelmode virtuel
0x00000000noyau / vecteurs d'interruptionsprocessus utilisateur
0x80000000registres d'entrées/sorties 
0xC0000000 noyau
0xFE000000 registres d'entrées/sorties