Processeur ARM Cortex 1


1 - Présentation de la Famille des processeurs Cortex

L’architecture ARM Cortex est une architecture brevetée, fabriquée par différents fondeurs ( ST, NXP, TI, … ).
Les modèles proposés s’adaptent aux applications en fonction des performances exigées en temps de calcul, consommation, prix, supports pour OS.

famille_cortex.svg

Exemple d’Application

mip2.png


2 - Equipement

2.1 - Matériel

2.2 - Logiciel

Plusieurs solutions sont possibles, je propose d’utiliser l’environnement de développement proposé par ST : STM32CubeIDE


3 - Les Registres

Nous retrouvons en vert les registres de mu0-risc :
L’architecture ARM-cortex comporte 13 registres de calcul ( R0-R12 )

registres_cortex.svg


4 - Quelques instructions ARM

L’objectif de cette partie n’est pas de passer en revue tous les instruction de l’architecture ARM Cortex, mais simplement d’en voir quelques unes pour coder des algorithmes simples.
L’assembleur reste uniquement un moyen pour bien comprendre les mécanismes dans un microprocesseur.
Nous passerons par la suite au langage C.

Toutes les instructions de calcul se font entre registres ( caractéristique d’une architecture RISC).
Il est possible dans une instruction d’indiquer une valeur immédiate ( en utilisant # )
Cette valeur doit tenir sur 8 bits ( ou pouvoir s’exprimer avec un décalage )

4.1 - Instructions Arithmétiques et Logiques

Reprenons les instructions évoquées dans la partie précédente, avec quelques suppléments ou variantes.

mov r1, #5 // r1 <- 5
mov r1, r2 // r1 <- R2

add r1,r2,#1 // r1 <- r2+1
add r1,r2,r3 // r1 <- r2+r3

sub r1,r2,#1 // r1 <- r2-1
sub r1,r2,r3 // r1 <- r2-r3

mul r1,r2,r3 // r1 <- r2*r3

and r1,r2,r3 // r1 <- r2 and r3
orr r1,r2,r3 // r1 <- r2 or r3

REMARQUE : Par défaut l’exécution d’une instruction ne modifie pas les bits d’état NZVC.
le suffixe s permet de modifier les bits d’état NZVC à l’issue d’un calcul :

adds r1,r2,#1 // r1 <- r2+1 ; modification(NZVC) ;

4.2 - Instruction de lecture/écriture mémoire de données

Lecture d’une donnée de 32 bits en mémoire

ldr r0,=val // r0 <- adr(val)
ldr r1, [r0] // r1 <- [r0]

Nous avons toujours le problème qu’une adresse fait 32 bits, et une instruction est codée sur 32 bits.
Je ne peux donc pas indiquer dans une même instruction le nom de l’instruction, un numéro de registre et une adresse mémoire de 32 bits .
La pseudo instruction ldr r0,=val est remplacé à la compilation par ldr r0,[PC+offset]. Le compilateur prendra soin lors de la compilation de placer l’adresse de la variable val à une case mémoire située à offset instructions de ldr r0,[PC+offset].

Lecture d’une donnée de 8 bits en mémoire

Comme chaque octet possède une adresse, l’instruction ldrb permet de rapatrier 8 bits dans un registre de 32 bits.
L’extension de format se fait par défaut en considérant des nombres non signés ( ajout de zéros sur les poids forts )

ldr r0,=val // r0 <- adr(val)
ldrb r1, [r0] // r1 <- [r0]

Ecriture d’une donnée de 32 bits en mémoire

ldr r0,=val
str r1, [r0]

Ecriture d’une donnée de 8 bits en mémoire

ldr r0,=val
strb r1, [r0]

Post Incrémentation

Lors du parcours d’un tableau, au lieu d’utiliser l’instruction add pour faire évoluer la valeur du pointeur, il est possible décrire :

ldr r1,[r0],#4 // r1 <- [r0] ; r0 <- r0+4

4.3 - Instructions de comparaison et de Saut ( Branch )

b loop // PC <- loop : Saut inconditionnel, dans tous les cas on va à l’adresse loop

Une séquence If then else ou une boucle conditionnelle ( for / while ) en langage C se traduit par un saut conditionnel en Assembleur.

  • Je fais un calcul ; ce calcul modifie les bits d’état NZVC
  • En fonction des bits NZVC, je fais le saut ou j’exécute l’instruction suivante

REMARQUE : l’instruction cmp r0, r1 permet de faire la modification des bits NZVC pour une soustraction r0-r1 ( le résultat n’est pas récupéré )

beq loop // PC <- loop si résultat précédent égal à 0 ( on regarde NZVC pour cela )
bne loop // PC <- loop si résultat précédent différent de 0 ( Not Equal )

conditions de saut :

conditions_saut.svg


5 - Premier Programme : Somme des éléments d’un Tableau

PROJET SOURCE

WORKSPACE_F411_ASM_STM32CUBE.zip

Pour tester les différents exemples ci-dessous, il faudra modifier le fichier makefile

5.1 - Environnement de Développement ( IDE )

Tutoriel STM32CubeIDE

L’ IDE STM32CUBE comporte :

  • Un compilateur : arm-none-eabi-gcc.
  • Un Debugger : arm-none-eabi-gdb.

STM32CUBE est basé sur le logiciel ECLIPSE

Le fichier Makefile donne les conditions de compilation et d’édition de liens.

5.2 - Système de fichiers d’un projet

└── nucleof411_base
    ├── config
    │   ├── ocd_st_nucleo_f4.cfg
    │   └── stm32f411re_flash.lds
    ├── include
    │   ├── board.h
    │   ├── cmsis
    │   ├── config.h
    │   └── stm32f411xe.h
    ├── main.bin
    ├── main.elf
    ├── main.elf.map
    ├── main.hex
    ├── Makefile
    ├── nucleof411_base Debug.launch
    ├── src
    │   ├── main.s
    └── startup
        ├── rcc.c
        ├── rcc.h
        ├── startup_stm32f411xe.s
        ├── stm32f411_periph.c
        ├── sys_handlers.c
        ├── sys_handlers.h
        ├── system_stm32f4xx.c

Reprenons le programme faisant la somme des éléments d’un tableau afin de le tester :

On distingue le segment “.data” et ‘.text" :

  • .data : Données initialisées, à placer en ROM (FLASH) pour les valeurs initiales et en RAM ( données modifiables au cours de l’exécution du programme ).
  • .text : Le code, à placer en ROM (FLASH)
//======================================================================
// VARIABLES GLOBALES
			.data
tab:			.word 	1,2,3,4,5
resultat:		.word	0
//======================================================================
// PROGRAMME
			.text
			.thumb
			.syntax unified
			.global main
main:
			mov  r4,#0      		// R4 <- 0
			ldr  r0,=tab 			// R2 <- addr tab
			mov	 r1,#5
LOOP:			ldr  r5,[r0]    		// val <- tab[R0]
   			add  r4,r4,r5   		// acc <- acc+val
			add  r0,r0,#4   		// R0++
			subs r1,r1,#1   		// compteur --
			bne  LOOP       		// PC <- PC - 12
			ldr  r0,=resultat 		// R0 <- addr resultat
			str  r4,[r0]    		// resultat <- acc

theend:			b   theend
//======================================================================     

5.3 - Compilation et placement mémoire

Lorsque je compile mon programme ( raccourcis CTRL+B, j’exécute mon makefile.
Le makefile fait référence au compilateur arm-none-eabi-gcc pour transformer des fichiers C/C++ ou Assembleur en fichiers objets binaires .o
L’éditeur de lien arm-none-eabi-ld permet de réaliser le placement mémoire de ces fichiers objet en tenant compte du fichier de configuration stm32f411re_flash.lds ( ce fichier indique dans quelle zone mémoire il faut placer les différents segments ( code, datas )). L’exécutable résultant main.elf peut alors être transmis à la cible.

compilation.svg

extrait de stm32f411re_flash.lds :

stm32f411re_flash.lds

_Min_Heap_Size = 0x200;;      /* required amount of heap  */
_Min_Stack_Size = 0x400;; /* required amount of stack */

/* Specify the memory areas */
MEMORY
{
FLASH (rx)      : ORIGIN = 0x08000000, LENGTH = 512K
RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS
{
  /* The startup code goes first into FLASH */
  .isr_vector :
  {
    . = ALIGN(4);
    KEEP(*(.isr_vector)) /* Startup code */
    . = ALIGN(4);
  } >FLASH

  /* The program code and other data goes into FLASH */
  .text :
  {
    . = ALIGN(4);
    *(.text)           /* .text sections (code) */
    *(.text*)          /* .text* sections (code) */
    *(.glue_7)         /* glue arm to thumb code */
    *(.glue_7t)        /* glue thumb to arm code */
    *(.eh_frame)

    KEEP (*(.init))
    KEEP (*(.fini))

    . = ALIGN(4);
    _etext = .;        /* define a global symbols at end of code */
  } >FLASH
....
  .data : 
  {
    . = ALIGN(4);
    _sdata = .;        /* create a global symbol at data start */
    KEEP(*(.data))           /* .data sections */
    *(.data*)          /* .data* sections */

    . = ALIGN(4);
    _edata = .;        /* define a global symbol at data end */
  } >RAM AT> FLASH

5.4 - Démarrage d’un programme : startup_stm32f411xe.s

A l’issue d’une mise sous tension ou d’un reset, PC reçoit l’instruction d’adresse Reset_Handler
On peut noter la boucle pour copier les données d’initialisation de la flash vers la RAM.
La fonction SystemInit() permet de configurer l’horloge du microcontrôleur.

startup_stm32f411xe.s

		.section  .text.Reset_Handler
		.weak	Reset_Handler
		.type	Reset_Handler, %function
Reset_Handler:
		// Copy the data segment initializers from flash to SRAM 
		ldr		r0, =_sidata
		ldr		r1, =_sdata
		ldr		r2, =_edata
dloop:		cmp		r1, r2
		beq		dloope
		ldr		r4, [r0], #4
		str		r4, [r1], #4
		b		dloop
		
dloope:
		// Zero fill the bss segment
		ldr		r0, =_sbss
		ldr		r1, =_ebss
		mov		r2, #0
bloop:		cmp		r0, r1
		beq		bloope
		str		r2, [r0], #4
		b		bloop
bloope:		
		// Call the clock system intitialization function.
		bl		SystemInit
		
		// Call static constructors
		bl		__libc_init_array
		
		// Call the application's entry point
		bl		main
_exit:		b		_exit

5.5 - Test d’un Programme

Après avoir compilé et donc généré un exécutable .elf ( à condition qu’il n’y ait pas eu d’erreur de compilation ), nous pouvons charger et debugger notre programme.

La configuration de debug se trouve dans Run –> Debug Configurations ; nous y faisons référence à main.elf.

debug_config.png

Accès direct :

3.png

Pour placer un point d’arrêt, double cliquer dans la marge ( ou appuyer sur SHIFT+CTRL+B )
Pour aller directement au point d’arrêt, appuyer sur F8 (Resume).
Mode pas à pas pour analyser l’exécution de chaque instruction :

  • F5 : Step Into : pas à pas total
  • F6 : Step Over : pas à pas sans entrer dans les fonctions ( mais en les exécutant quand même ).

Dans l’environnement de Debug, les registres sont visibles dans la fenêtre Registers ( Window -> Show View -> Registers )

registers.png

Pour observer la mémoire, utiliser la fenere Memory Browser :

memory_browser.png


6 - Appel d’une fonction

Dans le makefile, effectuer la modification suivante :

ASRC += src/main_calc_2.s ### A MODIFIER ###

Instruction BL ( Branch with Link)

//======================================================================
// VARIABLES GLOBALES
			.data
tab:			.word 	1,2,3,4,5
resultat:		.word	0

//======================================================================
// PROGRAMME
			.text
			.thumb
			.syntax unified
			.global main, sum_tab
main:
			ldr	r0,=tab
			mov	r1,#5

			bl	sum_tab			// Appel fonction sum_tab

			ldr	r1,=resultat
			str	r0,[r1]    		// resultat <- acc

theend:			b	theend
//-----------------------------------------------------------------------
sum_tab:		mov	r4,#0      		// R3 <- 0
LOOP:			ldr	r5,[r0]    		// val <- tab[R0]
   			add	r4,r4,r5   		// acc <- acc+val
			add	r0,r0,#4   		// R0++
			subs	r1,r1,#1   		// compteur --
			bne	LOOP       		// PC <- PC - 12

			mov	r0,r4
			mov	pc,lr
//======================================================================     

Lors de l’étude du langage C, nous avons évoqué l’intérêt du découpage d’un programme en fonctions ( rangement et réutilisation ).
Une fonction reste un bout de code situé à une certaine adresse.
Pour y accéder je dois donc effectuer un saut.
L’instruction b pourrait être utilisée, mais après exécution de la fonction, il faut retourner à l’instruction suivant l’appel de la fonction.
Je doit donc mémoriser l’adresse de retour.

L’instruction bl permet donc de :

  • Effectuer un saut à une certaine adresse
  • Mémoriser l’adresse de retour dans le registre R14 ( LR - Link Register )

Par convention, les paramètres des fonctions sont placés dans R0 et R1 ( puis R2, R3 si nécéssaire ).
Le paramètre de retour de fonction est placé dans R0.

appel_fonction.svg


7 - Traitement d’une Chaine de caractères

Dans le makefile, effectuer la modification suivante :

ASRC += src/main_str.s ### A MODIFIER ###

Représentation d’une chaine de caractères dans un processeur : le code ASCII

Comme toute donnée dans un système à processeurs, les caractères sont codés avec des 0 et des 1.

ascii.svg

Algorithme : passage de minuscules à majuscules

Le codage du caractère ‘a’ est 0x61
Le codage du caractère ‘A’ est 0x41
Pour toute lettre, il y a une différence de 0x20 entre la minuscule et la majuscule.

Considérons la chaîne de caractères “abcdefgh”, et observons la en mémoire :

chaine.png

C’est un tableau d’octets, dont l’adresse du premier élément est 0x200000b0
Le dernier élément du tableau est le caractère Nul ( 0x00 ).

L’algorithme consiste donc à :

  • accéder à chaque caractère en mémoire ( avec l’instruction ldrb )
  • soustraire 0x20 à la valeur récupérée
  • écrire en mémoire le valeur modifiée ( avec strb )
//======================================================================
// VARIABLES GLOBALES
			.data
chaine:		.asciz 	"abcdefgh"

//======================================================================
// PROGRAMME
			.text
			.thumb
			.syntax unified
			.global main, change_casse
main:
			ldr  r0,=chaine
			bl	 change_casse

theend:			b   theend
//-----------------------------------------------------------------------
change_casse:

			ldrb  r4,[r0]
			cmp   r4,#0
			beq	  fin_chaine
			sub	  r4,r4,#0x20
			strb  r4,[r0],#1
			b	  change_casse

fin_chaine: 		mov	  pc,lr
//-----------------------------------------------------------------------


8 - TRAVAUX PRATIQUES

PROJET SOURCE

WORKSPACE_LAB_F411_ASM_STM32CUBE.zip

Traitement d’une chaine de caractères

Dans le makefile, effectuer la modification suivante :

# SRC += src/main.c
ASRC += src/main_remove_space.s
# ASRC += src/int2ascii.s

Q1. Proposer un codage de la fonction remove_space permettant de remplacer tout espace dans une chaine de caractères ( ch_src ) par un undescore (’_’).
La chaîne modifiée sera placée à l’adresse ch_dest.