Développer sur microcontrôleur sans microcontrôleur : les émulateurs

Magazine
Marque
GNU/Linux Magazine
HS n°
Numéro
103
Mois de parution
juillet 2019
Spécialité(s)


Résumé

Le monde fascinant du développement sur systèmes embarqués – des petits microcontrôleurs 8 bits aux processeurs capables d’exécuter GNU/Linux – est accessible même sans posséder de plateforme matérielle dédiée, grâce aux émulateurs. Nous proposons un cheminement progressif, du petit ATmega32U4 avec le tracé des chronogrammes des signaux internes, au STM32 programmé en C ou exécutant NuttX, pour aborder le tout nouveau RISC-V exécutant FreeRTOS et finalement, voir GNU/Linux tourner sur RISC-V, MIPS ou ARM.


Body

Il est bien connu que les développeurs de matériel n’aboutissent jamais à temps à faire fonctionner leur circuit. Que ce soit une erreur de routage, un problème d’approvisionnement ou de réalisation de circuit, le matériel n’est jamais disponible à temps. En attendant, le développeur de logiciel attend... ou pas. Nous allons présenter quelques émulateurs de processeurs, dans un contexte un peu plus serein que la lutte fratricide des hardeux contre les softeux : un émulateur permet de sonder l’état de registres difficilement accessibles sinon (traceur de signaux dans l’émulateur ATmega), voire de conseiller le développeur dans sa stratégie d’initialisation des signaux (horloges dans l’émulateur qemu appliqué au STM32). Cependant, un émulateur n’est jamais aussi bon que sa capacité à convenablement émuler les signaux du microprocesseur, et l’open source prend une fois de plus tout son sens pour corriger des dysfonctionnements : nous corrigerons un signal mal implémenté, qui se traduit par le blocage d'une bibliothèque populaire pour les programmeurs sur processeur ARM. Finalement, nous verrons que l’émulateur permet d’appréhender des architectures encore balbutiantes (RISC-V) ou moins courantes (MIPS) que les x86/ARM dont nous sommes abreuvés quotidiennement.

L’objectif de cet article est d’illustrer l’utilisation d’émulateurs, pour permettre au lecteur ne possédant pas de matériel de se familiariser avec le développement sur microcontrôleurs. Les trois cibles logicielles sont la programmation bas-niveau en C (baremetal), la surcouche FreeRTOS, qui permet d’appréhender les concepts de gestion de tâches et de la cohérence de l’accès concurrent aux ressources et finalement GNU/Linux, embarqué sur cibles MIPS et ARM. Nous suivons ainsi une évolution « logique » des niveaux d’abstraction, en partant du plus bas niveau pour élever l’abstraction vers les environnements exécutifs, puis le système d’exploitation.

Appréhender une nouvelle architecture prend un peu de temps et de patience, pour se familiariser avec les divers registres et subtilités d’une architecture donnée. Alors qu’un petit microcontrôleur 8-bits, tel que le Atmel (maintenant Microchip) ATmega n’a pas beaucoup de surprise à livrer autre que son architecture Harvard, un ST Microelectronics STM32 est bien plus surprenant, avec sa granularité fine de distribution d’horloges désactivées par défaut. RISC-V vient d’exploser aux yeux du grand public, tandis que ARM continue à lutter avec MIPS et SPARC : maîtriser ces architectures est gage de liberté et de capacité à sélectionner la meilleure plateforme pour un objectif donné. Ces diverses architectures peuvent se tester sur émulateur avant d’investir dans du matériel, tel que nous le verrons ici.

1. Microcontrôleur 8 bits ATmega32U4

1.1 Le simulateur simavr

simavr (http://github.com/buserror/simavr, aussi disponible en paquet Debian, mais d’un intérêt discutable si on n’en consulte pas les sources) est un simulateur de microcontrôleurs Atmel d’architecture Harvard (ATmega) incluant l’ATmega32U4, qui équipe les plateformes Olimexino32U4 que nous utilisons pour l’enseignement introductif aux microcontrôleurs en Licence3. Ainsi, l’émulateur offre un outil pédagogique indéniable, car sans prétendre pallier la manipulation sur plateformes expérimentales open hardware au coût modeste (13 euros/unité), il permet de reproduire chez soi les travaux pratiques ne faisant pas appel à une interaction avec le matériel. simavr est de plus capable de générer des traces exactes en temps pour suivre l’évolution de signaux internes au microcontrôleur – fonction supportée par les sondes JTAG pour les microcontrôleurs équipés de cette fonction (broches partagées avec le port F sur l’ATmega32U4), mais avec une souplesse additionnelle de pouvoir intercepter une manipulation erronée de registres non initialisés, par exemple.

Prenons un programme trivial chargé de faire clignoter une LED connectée à une broche de port d’entrée-sortie généraliste (GPIO), ici la broche 5 du port C :

01: #include <avr/io.h> //E/S ex PORTB
02: #define F_CPU 16000000UL
03: #include <util/delay.h>
04: int main(void){
05:   DDRC |=1<<PORTC5;
06:   PORTC|=1<<PORTC5;
07:   while (1){PORTC^=1<<PORTC5;_delay_ms(10000);}
08:   return 0;
09: }

Générer les traces indiquant l’évolution d’un signal interne nécessite l’ajout, dans le code source du programme à destination du microcontrôleur, d’un en-tête déclarant les signaux à observer de la forme :

01: #include "avr_mcu_section.h"
02: AVR_MCU(F_CPU, "atmega32");
03: const struct avr_mmcu_vcd_trace_t _mytrace[] _MMCU_ = {
04:  { AVR_MCU_VCD_SYMBOL("PORTC"), .what = (void*)&PORTC, }, };

Ceci ne sera pas compilé dans le binaire final à destination du microcontrôleur. Ces lignes de code informent simavr que nous désirons observer le port C et effectivement, la simulation du programme proposé auparavant au moyen de la commande simavr -f 16000000 -m atmega32u4 programme.out se traduit par un chronogramme, dans lequel le port C change périodiquement d’état (voir figure 1). Les arguments de simavr sont -f, pour définir la fréquence du processeur et -m, pour le type de processeur.

À chaque simulation, un fichier trace_file.vcd est généré. Ce fichier s’analyse au moyen de gtkwave (tout comme les chronogrammes de simulation du comportement d’un code VHDL sur FPGA). Une fois gtkwave lancé, cliquer sur logic pour faire apparaître les signaux acquis dans la fenêtre en dessous. Faire glisser chacun des signaux dans la fenêtre principale des chronogrammes et cliquer sur la loupe munie d’un carré, pour se placer pleine échelle.

fig1

Fig. 1 : Chronogramme issu d’une simulation simavr et affiché dans gtkwave, ici un simple clignotement toutes les 10 ms sur un GPIO.

1.2 Communications asynchrones (RS232)

Une liaison USB est complexe et requiert une bibliothèque gourmande en ressources pour déclarer ses interfaces de communication (endpoints). Néanmoins, tout circuit électronique numérique comportant un microcontrôleur est muni d’une interface de communication très simple et facile à déverminer, bien que plus lente que l'USB, implémentant le protocole série asynchrone compatible RS232 [1], qui permet de démarrer le système embarqué, avant qu’un système d’exploitation ou une bibliothèque lourde ne donne accès aux protocoles de communication rapides, mais complexes. Nous nous proposons donc d’observer les transactions selon le protocole RS232 pour quelques applications de communication de base, et ce, non sur microcontrôleur physique, mais sur émulateur. En effet, simavr émule le comportement d’un ATmega(32U4) et fournit, en plus des ressources disponibles sur le « vrai » composant, accès à l’état de ses registres internes.

01: #include <avr/io.h> //E/S ex PORTB
02: #define F_CPU 16000000UL
03: #include <util/delay.h>
04: 
05: #include "avr_mcu_section.h"
06: AVR_MCU(F_CPU, "atmega32");

07: AVR_MCU_VCD_FILE("trace_file.vcd", 1000);

08: const struct avr_mmcu_vcd_trace_t _mytrace[] _MMCU_ = {

09:  { AVR_MCU_VCD_SYMBOL("UDR1"), .what = (void*)&UDR1, },

10: };

Ces premières lignes, en plus de définir quelques fonctions fournies par avr-libc, indiquent à simavr d’exporter la trace représentant le registre de transmission de données de l’interface asynchrone UDR1, situé à l’adresse 0xCE [2, p. 388] : écrire une valeur dans ce registre se traduit par l’activation de la machine à états, qui va générer les niveaux successifs sur la broche TXD1 (PD3), pour transmettre la donnée sans supervision explicite du logiciel.

Suivent les fonctions de communication : comme tout protocole asynchrone, nous devons déterminer le débit de transfert (baudrate), que nous sélectionnerons entre deux valeurs standard que sont 57600 bauds ou 9600 bauds :

01: #define UART_BAUDRATE (57600)
02: //#define UART_BAUDRATE (9600)
03:

04: void init_uart(void)

05: {unsigned short baud;

06:  UCSR1A = (1<<UDRE1);  // importantly U2X1 = 0
07:  UCSR1B = (1 << RXEN1)|(1 << TXEN1); // enable receiver and transmitter

08:  UCSR1C = (1<<UCSZ11)|(1<<UCSZ10); // 8N1

09:  // UCSR1D = 0; // no rtc/cts

10:  baud = (((( F_CPU / ( UART_BAUDRATE * 16UL))) - 1));

11:  UBRR1H = (unsigned char)(baud>>8);
12:  UBRR1L = (unsigned char)baud;

13: }

14:

 15: void uart_transmit (unsigned char data)

16: {while (!( UCSR1A & (1<<UDRE1))); // wait while register is free

17:  UDR1 = data; // load data in the register

18: }
19:

20: void send_byte(unsigned char c)

21: {unsigned char tmp;

22:  tmp=c>>4; if (tmp<10) uart_transmit(tmp+'0'); else uart_transmit(tmp+'A'-10);
23:  tmp=(c&0x0f); if (tmp<10) uart_transmit(tmp+'0'); else uart_transmit(tmp+'A'-10);
24: }

25:

26: void send_short(unsigned short s)
27: {send_byte(s>>8); send_byte(s&0xff);}

Le port série est configuré pour communiquer 8 bits/donnée, sans bit de parité et 1 bit d’arrêt, auquel on pensera à ajouter le bit implicite de départ, soient au total 10 bits par donnée transmise. Nous devrions donc attendre 1000/960=1,042 ms (9600 bauds) ou 1000/5760=0,174 ms (57600 bauds) par donnée transmise.

f2

Fig. 2 : Chronogrammes des communications asynchrones – changement d’état de UDR1 – pour un débit de communication de 57600 bauds (haut), nécessitant 0,188 ms pour transmettre un octet ou 9600 bauds (bas), nécessitant 1,234 ms pour transmettre un octet.

simavr sauve donc l’évolution du registre, dont le chronogramme est tracé en figure 2. Le point intéressant ici est le respect des contraintes temporelles de la communication, considérablement plus lente que tout calcul sur le microcontrôleur. Nous constatons qu’à 9600 bauds, un octet est transmis en 1,234 ms (soit près de 20000 cycles d’horloge du processeur cadencé à 16 MHz) ou à 57600 bauds à 0,188 ms. Ces durées correspondent à 874 octets/seconde (contre 960 octets/seconde en théorie) ou 5313 octets/seconde (contre 5760 octets/seconde en théorie), un résultat tout à fait acceptable, compte tenu des activités annexes de maintenance exécutées par ailleurs par le microcontrôleur.

1.3 Pointeur de pile

Simuler un GPIO est un peu trivial et sans intérêt pratique. Simuler un port de communication est mieux. Plus intéressant encore, les microcontrôleurs de la gamme ATmega ont la propriété de stocker le pointeur de pile dans deux registres (deux registres 8 bits, pour pointer sur une adresse de 16 bits, pour adresser plus de 256 octets de mémoire) qui peuvent donc être observés par simavr, comme tout port. Pour rappel, la pile est la zone mémoire utilisée comme brouillon par le processeur, et la corruption de pile est un problème classique de plantage de programme, ne serait-ce que parce que le processeur empile l’adresse de retour du compteur de programme, pour se rappeler où continue l’exécution d’un programme qui a sauté dans une fonction ou une procédure. Comme la pile sert aussi au stockage des variables locales aux fonctions (au contraire du tas, qui stocke les variables globales ou à portée de la durée de l’exécution du programme – préfixe static lors de la déclaration), une pile qui sort de l’espace adressable de mémoire se traduira par un plantage irrémédiable du programme : connaître l’état de sa pile est donc un élément clé pour garantir le bon fonctionnement d’un programme.

Nous nous intéressons désormais au pointeur de pile sur 16 bits, défini comme deux registres de 8 bits situés aux adresses 0x3D et 0x3E [2, p. 388]. Ainsi, nous modifions la configuration de la sauvegarde des traces par simavr, en complétant le code d’initialisation par :

01: #include "avr_mcu_section.h"

02: AVR_MCU(F_CPU, "atmega32");

03: AVR_MCU_VCD_FILE("trace_file.vcd", 1000);

04: const struct avr_mmcu_vcd_trace_t _mytrace[] _MMCU_ = {

05:  { AVR_MCU_VCD_SYMBOL("UDR1"), .what = (void*)&UDR1, },
06:  { AVR_MCU_VCD_SYMBOL("STACKL"), .what = (void*)(0x5D), }, // stack=0x3{DE}+0x20

07:  { AVR_MCU_VCD_SYMBOL("STACKH"), .what = (void*)(0x5E), }, // stack=0x3{DE}+0x20

08: }

Ceci ajoute à UDR1 les deux octets du pointeur de pile (stack pointer) STACKL (octet de poids faible) et STACKH (octet de poids fort). Nous observons (voir figure 3) que la pile est initialisée en haut (fin) de RAM, puisqu’empiler une variable correspond à décrémenter le pointeur de pile.

f3

Fig. 3 : Initialisation du pointeur de pile à 0xAFF par avr-gcc, au sommet de la pile comme l’indique la carte des plages mémoires de l’ATmega32U4 émulé ici.

Un exemple classique d’occupation de pile qui explose est un programme récursif, dans lequel une fonction est appelée une multitude de fois, par exemple dans le cas du calcul du factoriel d’un nombre :

formule_01

Une telle fonction factoriel() est implémentée dans le programme qui suit. À titre pédagogique, nous attendons un peu entre deux itérations (_delay_ms()), pour bien séparer les appels de la fonction dans les chronogrammes et allouons un tableau inutilisé, pour illustrer l’occupation de la pile pour conserver les variables temporaires locales à une fonction :

01: int factoriel(int n)
02: {volatile char inutile[0x32];
03:  _delay_ms(1);
04:  PORTB^=0xff;
05:  if (n==1) return(1);
06:   else return (n*factoriel(n-1));
07: }
08:
09: int main()
10: {long p;
11:  DDRB=0xff;
12:  _delay_ms(2);
13:  p=factoriel(8);
14:  _delay_ms(2);
15:  init_uart();
16:  send_short(p>>16);
17:  send_short(p&0xffff);
18:  uart_transmit('\n');
19:  while (1){}
20: }

Afin d’interdire à gcc d’éliminer le tableau inutilisé, nous préfixons sa définition du mot clé volatile, qui interdit au compilateur de faire une hypothèse sur l’utilisation de cette variable et nous compilons avec l’option d’optimisation -O0, pour interdire à gcc de transformer la fonction récursive en une itération au sein de la même fonction (remplacement de l’appel à la fonction par call, qui empile l’adresse du pointeur de programme par une boucle, itérée par un simple saut jmp par gcc, lors de son optimisation du code). Nous passons outre le message avertissant que la fonction _delay_ms() ne sera pas exacte en durée avec cette option d’optimisation, ce point n’étant pas le sujet de la démonstration.

Les chronogrammes des premières itérations sont fournis en figure 4. Chaque appel à la fonction factoriel() se traduit par un décrément du pointeur de pile de 0xD7-0x95=66 octets, un peu plus que les 0x32=48 octets nécessaires à stocker le tableau inutile. Cette allocation se répète au cours des trois premières itérations de l’appel à factoriel(), occupant petit à petit la RAM disponible (l’ATmega32U4 ne propose que 2,5 KB de RAM compris entre 0x100 et 0xAFF, figure 3). À la quatrième itération de factoriel(), l’octet de poids fort du pointeur de pile est décrémenté de 0x0A à 0x09.

f4

Fig. 4 : Évolution de la pile lors des appels à la fonction factoriel(), avec décrément du pointeur de pile pour allouer l’espace de la variable locale inutile à chaque nouvelle itération du calcul (rectangles rouges), jusqu’à atteindre la modification de l’octet de poids fort du pointeur de pile (rond rouge).

Finalement, les sources de simavr sont limpides et leur manipulation est aisée pour ajouter ses propres fonctionnalités. Par exemple, pour imposer un rythme plus soutenu de communication, qui ne se contente pas d’afficher les messages transmis sur port de communication asynchrone à chaque retour chariot ou lorsque le tampon est plein, nous avons décidé de modifier simavr/sim/avr_uart.c avec des instructions du type if ((v>' ') && (v<127)) printf("%c",v); else printf("0x%02x",v); fflush(stdout); dans les fonctions avr_uart_udr_write() et avr_uart_write(). De la même façon, si on ne veut pas passer par l’analyseur de chronogrammes, mais afficher l’état des GPIOs en cours d’exécution, on manipulera simavr/sim/avr_ioport.c dans avr_ioport_write() avec printf("\nSIMAVR: IOPORT @0x%02x<-0x%02x\n",addr,v);fflush(stdout); pour fournir l’état du GPIO. Ce faisant, l’exécution de la simulation dans simavr se traduit par :

$ simavr -f 16000000 -m atmega32u4 simulation_stack.out

Loaded 1956 .text at address 0x0

Loaded 0 .data

SIMAVR: IOPORT @0x25<-0xff

SIMAVR: IOPORT @0x25<-0x00

SIMAVR: IOPORT @0x25<-0xff

SIMAVR: IOPORT @0x25<-0x00

SIMAVR: IOPORT @0x25<-0xff

SIMAVR: IOPORT @0x25<-0x00

SIMAVR: IOPORT @0x25<-0xff

SIMAVR: IOPORT @0x25<-0x00

0x200x18

FFFF9D80

La solution 0x9d80=40320 est bien 8! tandis que l’état du port B – dont l’emplacement est à l’adresse 0x25 – est immédiatement indiqué en cours d’exécution du simulateur, sans devoir passer par gtkwave. Le lecteur peut ainsi agrémenter le simulateur de diverses conditions de validité du programme, par exemple vérifier qu’une broche a bien été configurée en sortie, si son état est manipulé ou vérifier que le débit de communication a été configuré, si une liaison asynchrone est engagée.

simavr est donc parfaitement fonctionnel pour simuler le comportement de petits microcontrôleurs 8-bits d’architecture Harvard. Il s’agit là de cas bien particuliers ne proposant pas beaucoup de fonctionnalités au-delà de petits automates relativement simples. Plus général et surtout supportant des architectures bien plus puissantes, qemu ouvre un horizon bien plus vaste d’activités.

2. qemu pour STM32

L’émulateur simavr est fort sympathique pour un petit microcontrôleur 8 bits tel que la série des ATmega, mais manque de généralité : qemu supporte multitude d’architectures, dont le STM32 de ST Microelectronics grâce à l’effort d’André Beckus. Nous avons déjà largement présenté cette version de qemu – et sa déclinaison pour Eclipse – dans [3] et ne le mentionnons ici que pour insister sur le fait que la qualité d’un émulateur ne vaut que par le détail de son émulation. Nous avons récemment rencontré une erreur d’implémentation du convertisseur analogique numérique qui avait pourtant toujours donné satisfaction, mais dont l’émulation ne répondait pas aux attentes de la bibliothèque libre pour ARM Cortex libopencm3. Ce dysfonctionnement fut l’occasion de corriger qemu en conséquence : http://github.com/beckus/qemu_stm32/issues/24.

Notre attrait pour qemu porté au STM32 tient dans les messages d’erreurs inclus lors de l’utilisation de périphériques non initialisés. En effet, le STM32 désactive par défaut l’horloge cadençant tout périphérique, et une erreur classique de débutant sur cette plateforme est d’oublier d’initialiser la source d’horloge adéquate, se traduisant par un plantage du programme tentant de sonder le statut d’un bit, qui ne changera jamais d’état si le périphérique n’est pas cadencé. L’analyse du code de l’instrument commercialisé par la société SENSeOR sur qemu a ainsi permis de corriger un certain nombre d’erreurs d’ordre d’initialisation de périphériques qui finissaient par tout de même fonctionner, mais au risque de rencontrer un dysfonctionnement si un événement se déclenchait au cours de cette initialisation. Prenons de nouveau un exemple très simple pour STM32F103 qui se lie avec libopencm3 (http://github.com/libopencm3/libopencm3), que nous découpons étape par étape pour faire le lien avec l’émulateur.

2.1 C avec libopencm3

Après bien des évolutions de son arborescence, compte tenu de la multitude de plateformes ARM-Cortex supportées (M3, mais aussi M4), une solution stable semble avoir été trouvée, dans laquelle les fonctions associées aux divers périphériques sont annoncées en préfixant le chemin du répertoire par le nom du processeur concerné :

01: #include <libopencm3/stm32/rcc.h>
02: #include <libopencm3/stm32/adc.h>
03: #include <libopencm3/stm32/gpio.h>
04: #include <libopencm3/stm32/usart.h>

L’initialisation des GPIO est classique, mais doit répondre aux broches simulées dans qemu. Le STM32 exécutera :

01: void Led_Init(void) {gpio_set_mode(GPIOC,GPIO_MODE_OUTPUT_2_MHZ,GPIO_CNF_OUTPUT_PUSHPULL,GPIO12);}
02: void Led_Hi(void) {gpio_set (GPIOC, GPIO12);}
03: void Led_Lo(void) {gpio_clear(GPIOC, GPIO12);}

Ceci fait le pendant, dans qemu, à la déclaration d’un GPIO par (http://github.com/beckus/qemu_stm32/blob/stm32/hw/arm/stm32_p103.c#L122) :

led_irq = qemu_allocate_irqs(led_irq_handler, NULL, 1);

qdev_connect_gpio_out(gpio_c, 12, led_irq[0]); // GPIO C12

Il est ainsi extrêmement aisé de rajouter ses propres GPIO et les messages associés indiquant leur état. De la même façon, l’initialisation des ports de communication asynchrone par libopencm3 est :

01: void Usart1_Init(void)
02: {// Setup GPIO pin GPIO_USART1_TX/GPIO9 on GPIO port A for transmit. */
03: gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ,
04:  GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, GPIO_USART1_TX);
05:  usart_set_baudrate(USART1, 115200);
06:  usart_set_databits(USART1, 8);
07:  usart_set_stopbits(USART1, USART_STOPBITS_1);
08:  usart_set_mode(USART1, USART_MODE_TX);
09:  usart_set_parity(USART1, USART_PARITY_NONE);
10:  usart_set_flow_control(USART1, USART_FLOWCONTROL_NONE);
11:  usart_enable(USART1); // PA9 & PA10 for USART1

Ces lignes font le pendant de l’initialisation des ports de communication dans qemu (http://github.com/beckus/qemu_stm32/blob/stm32/hw/arm/stm32_p103.c#L129) par :

stm32_uart_connect((Stm32Uart *)uart2,serial_hds[0],STM32_USART2_NO_REMAP);

Ce qui permet donc ici encore de facilement déclarer des ports de communication additionnels, si nécessaires. Quelques fonctions de base sur le microcontrôleur simplifieront les échanges entre le programme et le terminal chargé d’afficher les messages :

01: void uart_putc(char c) {usart_send_blocking(USART1, c);} // USART1: send byte
02:
03: void affchar(char c)
04: {char b;
05:  b=((c&0xf0)>>4); if (b<10) uart_putc(b+'0'); else uart_putc(b+'A'-10);
06:  b=(c&0x0f); if (b<10) uart_putc(b+'0'); else uart_putc(b+'A'-10);
07: }
08:
09: void affshort(short s) {affchar((s&0xff00)>>8); affchar(s&0xff);}

Finalement, le périphérique le plus intéressant, mais qui a posé le plus de soucis dans son émulation : le convertisseur analogique numérique. Son initialisation dans libopencm3 se fait sans grande surprise :

01: void adc_setup(void)
02: volatile int i;
03:  gpio_set_mode(GPIOA, GPIO_MODE_INPUT, GPIO_CNF_INPUT_ANALOG, GPIO0);
04:  gpio_set_mode(GPIOA, GPIO_MODE_INPUT, GPIO_CNF_INPUT_ANALOG, GPIO1);
05: 
06:  adc_power_off(ADC1); // Make sure the ADC doesn't run during config
07:  adc_disable_scan_mode(ADC1);
08:  adc_set_single_conversion_mode(ADC1);
09:  adc_disable_external_trigger_regular(ADC1);
10:  adc_set_right_aligned(ADC1);
11:  adc_set_sample_time_on_all_channels(ADC1, ADC_SMPR_SMP_28DOT5CYC);
12:  adc_power_on(ADC1);
13: 
14:  for (i=0;i<800000; i++) // Wait for ADC starting up.
15:  __asm__("nop");
16: 
17:  adc_reset_calibration(ADC1);
18:  adc_calibrate(ADC1);

Pourtant, le convertisseur analogique numérique n’est pas explicitement instancié par la plateforme stm32-p103, mais fait partie intégrante du cœur du microcontrôleur décrit dans http://github.com/beckus/qemu_stm32/blob/stm32/hw/arm/stm32.c par stm32_create_adc_dev(stm32_container, STM32_ADC1, 1, rcc_dev, gpio_dev, 0x40012400, 0);. Nous avons fait le choix, dans http://github.com/beckus/qemu_stm32/blob/stm32/hw/arm/stm32_adc.c#L700, d’émuler divers comportements de convertisseur analogique numérique selon le canal sélectionné, par exemple en fournissant une sinusoïde qui se veut exacte en période d’échantillonnage, grâce à l’utilisation de la représentation du temps par qemu (qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL)).

Notre contribution récente à qemu pour STM32 a porté sur l’analyse du code de conversion analogique numérique proposé par libopencm3, implémenté sous la forme :

01: unsigned short read_adc_naiive(unsigned char channel)
02: {int c;
03:  unsigned char channel_array[16];
04:  channel_array[0] = channel;
05:  adc_set_regular_sequence(ADC1, 1, channel_array);
06:  adc_start_conversion_regular(ADC1);
07:  while (! adc_eoc(ADC1));
08:  return adc_read_regular(ADC1);

09: }

Ce qui ne rendait jamais de résultat de mesure. En effet, dans notre implémentation initiale de l’émulation du convertisseur, nous nous étions contenté d’annoncer la fin de conversion (s->ADC_SR&=~ADC_SR_EOC;), mais avions omis de relever le drapeau testé par libopencm3 pour respecter les consignes du manuel d’utilisateur du STM32F103 [4, p. 231], à savoir s->ADC_CR2&=~ADC_CR2_SWSTART;. Ce faisant, nous respectons bien le test de libopencm3, qui vérifie la fin de conversion non pas en testant le bit du même nom (EOC), mais par (http://github.com/libopencm3/libopencm3/blob/master/lib/stm32/common/adc_common_v1.c#L695) :

while (ADC_CR2(adc) & ADC_CR2_SWSTART);

Le programme sur le microcontrôleur se conclut par l’initialisation des horloges des périphériques utilisés – problème classique de plantage d’un programme sur STM32, qui par défaut ne cadence pas ses périphériques :

01: void clock_setup(void)
02: {rcc_clock_setup_in_hse_8mhz_out_72mhz(); // STM32F103
03:  rcc_periph_clock_enable(RCC_GPIOC); // Enable GPIOC clock
04:  rcc_periph_clock_enable(RCC_GPIOD); // Enable GPIOD clock for F4 (LEDs)
05:  rcc_periph_clock_enable(RCC_GPIOA); // Enable GPIOA clock
06:  rcc_periph_clock_enable(RCC_ADC1);
07:  rcc_periph_clock_enable(RCC_USART1);
08: }
09:
10: int main(void)
11: {volatile int i;
12:  short res;
13:  clock_setup();
14:  Usart1_Init();
15:  Led_Init();
16:  adc_setup();
17: 
18:  while (1) {
19:   res=read_adc_naiive(1);
20:   uart_putc('0');uart_putc('x');
21:   affshort(res); uart_putc('\n'); uart_putc('\r');
22:   Led_Hi2();
23:   for (i = 0; i < 800000; i++) __asm__("NOP");
24:   Led_Lo2();
25:  }
26:  return 0;
27: }

Ce programme se traduit lors de sa simulation par :

$ qemu-system-arm -M stm32-p103 -serial stdio -serial stdio -kernel programme.bin

LED Off

0x045B

LED On

LED Off

0x04AA

LED On

LED Off

0x04F1

Ceci démontre bien l’émulation du convertisseur analogique numérique, de la communication asynchrone et d’une broche de GPIO. Si cependant, nous sommes suffisamment maladroits pour compléter la fonction principale par un affichage sur le second port de communication asynchrone usart_send_blocking(USART2, ’a’); nous nous faisons insulter par :

qemu stm32: hardware warning: Warning: You are attempting to use the ADC2 peripheral while its clock is disabled.

 

R00=40004800 R01=00000061 R02=40012400 R03=10000000

R04=08000974 R05=08000974 R06=00000000 R07=200067d8

R08=00000000 R09=00000000 R10=00000000 R11=00000000

R12=00000002 R13=200067d8 R14=08000243 R15=0800089e

PSR=80000173 N--- T svc32

qemu: hardware error: Attempted to write to USART_DR while UART was disabled.

CPU #0:

Ces lignes nous indiquent donc que nous tentons de manipuler les registres d’un périphérique dont l’horloge n’est pas initialisée, puis que nous tentons de communiquer par un port asynchrone qui n’a pas été configuré.

De la même façon, omettre l’initialisation du convertisseur analogique numérique en commentant adc_setup(); se traduit par :

qemu: hardware error: Attempted to start conversion while ADC was disabled

Ce message est suffisamment explicite pour corriger le problème. L’émulateur peut donc amener un réel bénéfice d’analyse de code en sondant l’état des registres, par rapport à JTAG qui se contente de fournir le statut du matériel, sans en analyser le comportement.

2.2 NuttX

Nous avions présenté NuttX [5] comme environnement exécutif visant la compatibilité POSIX, avec une empreinte mémoire compatible avec les plus petits STM32. La vidéo de http://www.youtube.com/embed/4t5pO8cJU9k présente l’émulation de NuttX dans qemu, qui s’avère simple à mettre en œuvre sur une plateforme ne requérant pas le support de l’horloge temps réel (RTC). En effet, nous constatons qu’en clonant le dépôt git de NuttX à la date de rédaction de cette prose :

$ git clone https://bitbucket.org/nuttx/apps.git

$ git clone https://bitbucket.org/nuttx/nuttx.git

Et en configurant pour une version minimaliste du STM32F103 :

cd nuttx/

./tools/configure.sh configs/stm32f103-minimum/nsh

make

L’exécution dans la version d’André Beckus de qemu se solde immédiatement par une invite de commande du Shell nsh :

$ [...]/qemu_stm32/arm-softmmu/qemu-system-arm -M stm32-p103 -serial stdio -serial stdio -serial stdio -kernel nuttx.bin

NuttShell (NSH)

nsh>

En l’état, nous ne pouvons pas fournir de commande à nsh en l’absence de gestion des entrées du clavier, mais le principe de base fonctionne.

3. qemu pour RISC-V

Alors qu’il semblait acquis que l’architecture ARM allait dominer le monde de l’électronique numérique embarquée avec sa panoplie de processeurs répondant aux besoins allant du petit automate aux téléphones mobiles, 2018 s’avère peut-être marquer la fin de cette hégémonie, avec la mise sur le marché d’une architecture qui couvait depuis 8 ans à Berkeley. Cette architecture libre (sous licence BSD), initialement proposée comme softcore sur FPGA, a été présentée dans une puce silicium contrôlant un ordinateur pour la première fois au FOSDEM en 2018 (http://archive.fosdem.org/2018/schedule/event/riscv/) par SiFive, et a donné lieu à une session dédiée en 2019 (http://fosdem.org/2019/schedule/track/risc_v/). Non content de remettre en cause l’hégémonie d’ARM en lui imposant de libérer l’implémentation de certains de ses cœurs (www.arm.com/resources/designstart/designstart-fpga), cette nouvelle architecture libre impose à d’autres architectures de suivre la tendance, en particulier MIPS (http://wavecomp.ai/mipsopen). Les années à venir promettent donc une compétition excitante entre les « anciennes » architectures (ARM, MIPS, SPARC) et les nouveaux venus de la gamme RISC-V (figure 5) [6], avec laquelle il est certainement judicieux de se familiariser, d’autant plus qu’une version combinant CPU et FPGA, dans la lignée du Zynq, est proposée par Microsemi (ex-Lattice : http://www.microsemi.com/product-directory/soc-fpgas/5498-polarfire-soc-fpga).

fig5

Fig. 5 : Logo du projet RISC-V.

En l’absence de circuit matériel implémentant un des cœurs RISC-V, nous allons nous familiariser avec le travail sur cette architecture sur l’émulateur qemu. RISC-V est évidemment supporté par gcc. Bien que GNU/Linux soit supporté sur cette architecture, nous nous focaliserons dans un premier temps sur sa programmation en C (baremetal) et au moyen de FreeRTOS (figure 6).

f6

Fig. 6 : R. Barry présente FreeRTOS, désormais acquis par Amazon Web Service, exécuté sur RISC-V. Cette session du FOSDEM a une fois de plus fait salle comble.

3.1 Programmation en C (baremetal)

L’utilisation en C d’un microcontrôleur nécessite d’accéder directement aux registres de communication pour interagir avec l’utilisateur, puisque les bibliothèques de plus haut niveau telles que newlib ne peuvent pas savoir quel périphérique une instruction de type printf() vise : cette fonction fait appel à write() qui est implémentée, dans le cas de newlib, par les stubs qui font pointer les écritures vers le bon périphérique.

En l’absence de ces stubs, une bibliothèque se charge pour nous de transmettre les écritures et lectures vers le périphérique émulé par l’entrée-sortie standard (stdio) : libfemto, disponible sur http://github.com/michaeljclark/riscv-probe. L’implémentation des fonctions d’entrée-sortie permettra notamment d’interagir avec le programme émulé par qemu, qui supporte officiellement (http://github.com/qemu/qemu) RISC-V lors de la compilation par
./configure –enable-debug –target-list=“riscv64-softmmu riscv32-softmmu” –disable-gtk –disable-sdl –disable-werror.

À titre d’échauffement, on pourra valider l’exécution de l’exemple « Hello World », nommé hello dans riscv-probe/build/bin/rv32imac/qemu-sifive_e, en fournissant comme format de machine à qemu l’option -M sifive_e et en prenant soin de demander -nographic, afin que l’affichage s’effectue sur le terminal courant. Une fois l’exécution achevée, nous quitterons l’émulateur du terminal comme nous le ferions depuis minicom par <Ctrl> + <a> puis <x>, nous rappelant ainsi que qemu affiche les liaisons sur le port série et non sur un écran qui pourrait équiper le système embarqué. On s’en rappellera lors de l’émulation de GNU/Linux, pour lequel nous redirigerons la console sur un port série virtuel.

Le résultat s’obtient en se plaçant dans le répertoire riscv-probe et en compilant toute l’arborescence :

riscv-probe$ make

...

riscv-probe$ [...]/riscv-qemu/riscv32-softmmu/qemu-system-riscv32 -M sifive_e -nographic \

   -kernel build/bin/rv32imac/qemu-sifive_e/hello

Ce qui se traduit par :

hello world

Basés sur cet exemple, nous pouvons commencer à analyser le comportement de la nouvelle architecture avec, par exemple, la taille d’un long et l’organisation des octets en mémoire :

01: #include <stdio.h>
02: int main()
03: { long x=0x12345678;
04:  char *c=(char*)&x;
05:  printf("\n%d\n",sizeof(long));
06:  printf("%hhx %hhx %hhx %hhx\n",c[0],c[1],c[2],c[3]);

Après compilation, dont nous avons identifié les options en exécutant make V=2 pour rendre le Makefile verbeux :

FEMTO=[...]/riscv-probe/

riscv64-unknown-elf-gcc -Os -march=rv32imac -mabi=ilp32 -mcmodel=medany -c hello.c

riscv64-unknown-elf-gcc -Os -march=rv32imac -mabi=ilp32 -mcmodel=medany -nostartfiles \

 -nostdlib -nostdinc -static -lgcc -T $(FEMTO)/env/qemu-sifive_e/default.lds \

 $(FEMTO)/build/obj/rv32imac/env/qemu-sifive_e/crt.o \

 $(FEMTO)/build/obj/rv32imac/env/qemu-sifive_e/setup.o hello.o \

 $(FEMTO)/build/lib/rv32imac/libfemto.a -o hello32

Ceci donne le résultat qu’un long est codé sur cette architecture 32 bits sur 4 octets, tandis que l’affichage des octets individuels compris dans un mot codé sur plusieurs octets indique que, puisque 0x78 apparaît en premier, l’octet de poids faible est à l’adresse la plus faible. Nous sommes donc dans un modèle de processeur little endian, organisation des données similaires de celles adoptées par Intel sur architecture x86, afin de charger en premier les octets de poids faible et donc, calculer lors des opérations arithmétiques les retenues, en même temps que les octets de poids plus fort sont chargés dans l’unité arithmétique et logique. Cependant, en compilant en 64 bits par (noter les options -march= et -mabi) :

riscv64-unknown-elf-gcc -Os -march=rv64imac -mabi=lp64 -mcmodel=medany -c hello.c

riscv64-unknown-elf-gcc -Os -march=rv64imac -mabi=lp64 -mcmodel=medany -nostartfiles \

 -nostdlib -nostdinc -static -lgcc -T $(FEMTO)/env/qemu-sifive_e/default.lds \

 $(FEMTO)/build/obj/rv64imac/env/qemu-sifive_e/crt.o \

 $(FEMTO)/build/obj/rv64imac/env/qemu-sifive_e/setup.o hello.o

 $(FEMTO)/build/lib/rv64imac/libfemto.a -o hello64

 Nous obtenons sous la version 64 bits de qemu le résultat suivant :

 $ [...]/riscv-qemu/riscv64-softmmu/qemu-system-riscv64 -M sifive_e -nographic -kernel hello64

 8

 00000078 00000056 00000034 00000012

Ceci indique cette fois qu’une variable de type long occupe 8 octets (64 bits), mais l’organisation reste évidemment little endian.

3.2 FreeRTOS

La version V10.2.0 de FreeRTOS, dont les évolutions sont décrites sur http://www.freertos.org/History.txt, publiée le 25 février 2019, supporte officiellement l’architecture de processeur RISC-V et en particulier, sa déclinaison 32 bits.

L’archive se télécharge sur http://sourceforge.net/projects/freertos/files/latest/download et la démonstration pour plateforme RISC-V se trouve dans le répertoire FreeRTOS/Demo/RISC-V-Qemu-sifive_e-FreedomStudio. Nous y trouvons les scripts nécessaires pour compiler un exemple, qu’il faut cependant modifier un peu pour les adapter à la chaîne de compilation fournie par http://github.com/riscv/riscv-gnu-toolchain – la chaîne de compilation s’obtient par make newlib, pour éviter de compiler toutes les dépendances inutiles. Nous remplaçons en effet dans BuildEnvironment.mk l’appel à riscv32-unknown-elf par riscv64-unknown-elf et ajoutons dans Makefile l’ordre de lier le binaire sur la version 32 bits de la bibliothèque libc, en ajoutant LDFLAGS += -march=rv32imac -mabi=ilp32 après la première occurrence de la variable d’environnement LDFLAGS autour de la ligne 128. En effet, le nom du compilateur ne présume pas de la taille des registres de la cible, mais c’est l’option -march qui détermine la nature de la cible (tout comme pour ARM, lorsque nous choisissons entre Cortex M3 ou M4). Le fichier résultant de la compilation de FreeRTOS est bien un binaire à destination d’un RISC-V :

RISC-V-Qemu-sifive_e-FreedomStudio$ file build/FreeRTOS-simple.elf

build/FreeRTOS-simple.elf: ELF 32-bit LSB executable, UCB RISC-V, version 1 (SYSV),

statically linked, with debug_info, not stripped

Lors de son exécution sous qemu, ce programme affiche périodiquement le même message :

[...]/RISC-V-Qemu-sifive_e-FreedomStudio$ riscv-qemu/riscv32-softmmu/qemu-system-riscv32 \

-M sifive_e -nographic -kernel build/FreeRTOS-simple.elf

core freq at 8628832 Hz

Blink

Blink

Blink

[...]

Au-delà de cet exemple trivial de démonstration de la capacité à compiler et exécuter un programme lié à l’ordonnanceur fourni par FreeRTOS, nous avons ici l’opportunité de découvrir les subtilités du développement collaboratif et en particulier, le partage des ressources. D’une part, la mémoire est une denrée rare, qu’il faut partager efficacement en l’absence de gestionnaire de mémoire capable de virtualiser les domaines d’adresses accessibles par chaque tâche et d’autre part, plusieurs tâches peuvent vouloir accéder à la même ressource, qu’il faut arbitrer par le mécanisme classique des MutEx (accès Mutuellement Exclusif aux ressources).

L’exemple qui suit reprend intégralement le code source fourni dans [3, section 4], pour faire appel à une émulation des accès aux ressources matérielles et ainsi rendre les codes portables – une méthode classique de séparation de la partie algorithmique du code (portable) et de la partie accédant aux périphériques (dépendante de chaque architecture). Dans notre cas, common.c et common.h, qui implémentent ces fonctions, sont disponibles sur http://github.com/jmfriedt/tp_freertos/tree/master/common et se contentent d’afficher un message sur la console pour indiquer qu’une ressource matérielle est sollicitée. Nous ne reproduisons pas ici le code dans son intégralité (voir [3]), mais précisons qu’il faut penser à fournir les diverses fonctions de maintenance requises par le portage au RISC-V de FreeRTOS :

01: void vApplicationMallocFailedHook( void );
02: vApplicationIdleHook( void );
03: vApplicationStackOverflowHook( TaskHandle_t pxTask, char *pcTaskName );
04: vApplicationTickHook( void );
05:

06: vApplicationStackOverflowHook( TaskHandle_t pxTask, char *pcTaskName )
07: {taskDISABLE_INTERRUPTS();
08:  for( ;; );
09: }

10:

11: void vAssertCalled( void )
12: {volatile uint32_t ulSetTo1ToExitFunction = 0;
13:  taskDISABLE_INTERRUPTS();
14:  while( ulSetTo1ToExitFunction != 1 ) __asm volatile( "NOP" );
15: }

16:

17: void vApplicationMallocFailedHook( void ) { for( ;; ); }

18: void vApplicationIdleHook( void ) { }
19: void vApplicationTickHook( void ) { }

Ces diverses fonctions appelées pour gérer les interruptions et nécessaires à l’ordonnanceur FreeRTOS sont reprises des exemples fournis dans l’archive de l’environnement exécutif, sans modification. L’exécution de ce programme dans qemu se traduit par le résultat escompté, à savoir (après avoir légèrement modifié la règle d’origine du Makefile, pour ajouter l’option -nographic et avoir retiré les directives de connexion à gdb, que sont -s -S) :

$ make sim-qemu

qemu-system-riscv32 -kernel build/FreeRTOS-simple.elf -nographic -machine sifive_e

core freq at 8624537 Hz

Usart_Init

Led_Init

Led_Hi1

Hello World

Led_Hi2

Led_Hi1

Led_Lo2

Hello World

Led_Lo1

Led_Hi2

Led_Lo2

De la même façon, nous reprenons intégralement le code permettant l’exploration des mutex de [3, section 4.3]. Pour rappel, nous avions proposé de protéger l’affichage de messages longs par mutex, afin d’en garantir l’intégrité.

En l’absence de mutex, nous obtenons parfois un message corrompu sur l’affichage :

1111111111111111111111111111111111111111111

1111111111111111111111111112222222222222222222222222222222222222222222

1111111111111111

1111111111111111111111111111111111111111111

Ceci ne survient jamais si nous prenons soin de protéger les appels à uart_puts() par les mécanismes appropriés, implémentés au niveau de l’ordonnanceur par xSemaphoreTake() et xSemaphoreGive().

Finalement, FreeRTOS permet d’afficher la liste des tâches, leur état et leur occupation de mémoire volatile sous forme d’allocations sur la pile, ainsi que de placer une signature (watermark) en fin de pile, pour en détecter la corruption. Ces fonctionnalités sont évidemment toujours opérationnelles sur architecture RISC-V, tel que nous nous en convainquons en activant les fonctions d’affichage de la liste des tâches par (en pensant à retirer l’option par défaut, qui désactive ces fonctions (#define configUSE_TRACE_FACILITY 0)) :

#define configUSE_TRACE_FACILITY 1

#define configUSE_STATS_FORMATTING_FUNCTIONS 1

Une fois FreeRTOS configuré de cette façon (make clean && make), l’appel à la fonction vTaskList(c); affiche la liste des fonctions et leur statut :

Led_Lo2

1111111111111111111111111111111111111111111

2222222222222222222222222222222222222222222

1111111111111111111111111111111111111111111

2222222222222222222222222222222222222222222

 

Hello World

ps         X   1   284   5

IDLE       R   0   22    6

t1         B   1   117   3

t2         B   1   117   4

LedFlash   B   2   73    2

LedFloat   B   1   73    1

Tmr Svc    B   6   20    7

Led_Hi1

1111111111111111111111111111111111111111111

2222222222222222222222222222222222222222222

Led_Hi2

Ainsi, tous les exemples proposés dans [3] sont reproductibles sur émulateur, sans avoir besoin d’acquérir le matériel correspondant.

4. GNU/Linux sur ARM, MIPS, et RISC-V

Nous ne pouvons conclure ce survol des émulateurs sans mentionner le support de GNU/Linux sur des architectures que nous ne possédons pas. Ainsi, le processeur ARM inclus dans le Zynq qui équipe la Redpitaya est émulé par qemu-system-arm, si nous exécutons qemu-system-arm -append “console=ttyPS0,115200 root=/dev/mmcblk0 rw earlyprintk” -M xilinx-zynq-a9 -m 1024 -serial mon:stdio -dtb zynq-red_pitaya.dtb -nographic -kernel uImage -sd rootfs.ext4 lorsque nous compilons une image de GNU/Linux au moyen de buildroot, tel que décrit sur http://github.com/trabucayre/redpitaya.

Au-delà de l’aspect pédagogique d’exécuter GNU/Linux sur une plateforme exotique, cette approche permet d’exécuter des firmwares disponibles uniquement sous forme binaire, pour en sonder les vulnérabilités. C’est l’approche sélectionnée par [8] pour analyser les logiciels embarqués des caméras communiquant sur IP et détecter les failles de sécurité. Ainsi, http://www.devttys0.com/2011/09/exploiting-embedded-systems-part-3/ décrit comment exécuter un logiciel embarqué de routeur wi-fi basé sur un processeur MIPS.

Finalement, RISC-V est officiellement supporté par buildroot : la configuration par make qemu_riscv64_virt_defconfig permet de générer une série de fichiers pour exécuter GNU/Linux sur qemu. L’ordre d’exécution est un peu verbeux avec
qemu-system-riscv64 -M virt -kernel output/images/bbl -append “root=/dev/vda ro console=ttyS0” -drive file=output/images/rootfs.ext2,format=raw,id=hd0 -device virtio-blk-device,drive=hd0 -device virtio-net-device,netdev=vmnic -netdev tap,id=vmnic,ifname=vnet0 -nographic en supposant que nous lançons qemu-system-riscv64 depuis le répertoire buildroot, puisque tous les fichiers générés par la compilation sont supposés être situés dans output/images. Le fichier bbl est le Berkeley Boot Loader.

Lors de la compilation d’un nouveau noyau par buildroot, il est judicieux d’effacer l’ancienne version du bootloader par rm -rf output/build/riscv-pk, afin de forcer sa recompilation à l’issue de la compilation du nouveau noyau.

Nous pourrons rapidement nous convaincre que l’architecture cible est la bonne, en consultant le contenu de /proc/cpuinfo, qui indique rv64imafdcsu : nous sommes sur la bonne voie.

Alors que la programmation en espace utilisateur ne présente aucune difficulté autre que d’identifier le compilateur dans output/host/usr/bin de buildroot comme nommé riscv64-linux-gcc, la partie module noyau est toujours un peu plus intéressante. En effet, nous constatons après avoir exécuté GNU/Linux sur qemu que le devicetree contient un certain nombre de nœuds, tel que décrits dans /sys/firmware/devicetree/base/ et en particulier, le périphérique virtio_mmio. Bien que l’outil devmem permette de sonder l’état des divers registres de ce périphérique depuis l’espace utilisateur et de vérifier la cohérence de leur contenu avec la documentation technique de http://docs.oasis-open.org/virtio/virtio/v1.0/csprd01/virtio-v1.0-csprd01.html, qui dit que le premier registre (adresse 0x00 lue par devmem 0x10001000) contient l’identifiant 0x74726976, il est toujours plus satisfaisant d’accéder aux ressources matérielles depuis un module noyau. Dans notre cas, l’exemple trivial d’accès au périphérique matériel depuis le noyau est implémenté par :

01: #include <linux/module.h> /* Needed by all modules */
02: #include <linux/kernel.h> /* Needed for KERN_INFO */
03: #include <linux/init.h> /* Needed for the macros */
04: #include <linux/ioport.h> // request_mem
05: #include <linux/io.h> // ioremap
06: 
07: #define IO_BASE1 0x10001000
08:

09: static void __iomem *jmf_gpio; //int jmf_gpio;
10:

11: int hello_start(void);
12: void hello_end(void);
13:

14: int hello_start()
15: {unsigned int stat;
16:  if (request_mem_region(IO_BASE1,0x12c,"GPIO cfg")==NULL)
17:   printk(KERN_ALERT "mem request failed");
18:  else
19:   {jmf_gpio=(void __iomem*)ioremap(IO_BASE1, 0x4);
20:   stat=readl(jmf_gpio+0x0);
21:   printk("stat=%x\n",stat);
22:   release_mem_region(IO_BASE1, 0x12c);
23:  }
24:  return 0;
25:

26:

27: void hello_end() {printk(KERN_INFO "Goodbye\n");}

28: 

29: module_init(hello_start);

30: module_exit(hello_end);

31: MODULE_LICENSE("GPL");

Ce code utilise ioremap() pour convertir l’adresse physique en adresse virtuelle et se compile par :

obj-m +=hello.o

all:

   make ARCH=riscv CROSS_COMPILE=riscv64-buildroot-linux-gnu- -C \

/home/jmfriedt/buildroot-riscv/output/build/linux-8fe28cb58bcb235034b64cbbb7550a8a43fd88be/ \

M=$(PWD) modules

Pour finalement donner :

# insmod hello.ko

[ 284.816000] stat=74726976

Nous pourrons donc sereinement tester nos développements noyau sur cette nouvelle architecture, sans avoir accès au matériel.

5. Séparation de l’algorithme et accès au matériel

Nous avons mentionné l’utilisation de qemu pour auditer le code d’un système embarqué commercialisé par SENSeOR et qualifier la bonne initialisation des divers périphériques. Une architecture de développement, qui allège considérablement le portage d’un logiciel embarqué (firmware) d’une architecture à une autre, est une séparation rigoureuse de la partie algorithmique (portable) de la partie accédant aux périphériques matériels (fortement dépendante de chaque cible). Ce faisant, nous bénéficions par ailleurs de la capacité à fournir des signaux synthétiques aux diverses entrées simulées et ainsi tester des configurations difficiles à reproduire expérimentalement, voire rejouer des scénarios de mesure enregistrés sur du matériel afin d’observer le comportement de divers algorithmes. À titre d’exemple de la conversion analogique numérique, sachant que le code pilotant les convertisseurs sur du matériel s’écrit sous la forme :

01: unsigned short interroge (unsigned short puissance, unsigned int freq,unsigned int offset,unsigned char chan)
02: { unsigned int v12;
03:  FTW0[1] = (freq & 0xFF000000) >> 24;
04:  FTW0[2] = (freq & 0xFF0000) >> 16;
05:  FTW0[3] = (freq & 0xFF00) >> 8;
06:  FTW0[4] = (freq & 0xFF);
07: [...]
08:  v12 = readADC12 ();
09:  readerF2_CLR; // coupe reception
10:  TIM_ITConfig (TIM3, TIM_IT_Update, ENABLE);

11:  return (v&0x03F);

12: }

Son émulation, par exemple sur PC, exploitera le même prototype de fonction, mais fournira des données synthétiques, par exemple par :

01: unsigned short interroge(unsigned short puissance,unsigned int freq, \
02: __attribute__((unused))unsigned int offset,__attribute__((unused))unsigned char chan)

03: {float reponse;
04:  reponse =exp(-(((float)freq-f01)/df)*(((float)freq-f01)/df))*3100.;
05:  reponse+=exp(-(((float)freq-f02)/df)*(((float)freq-f02)/df))*3100.;
06:  reponse+=(float)((rand()-RAND_MAX/2)/bruit); // ajout du bruit;
07:  if (reponse<0.) reponse=0.;
08:  if (reponse>4095.) reponse=4095.;
09:  usleep(60);
10:  return((unsigned short)reponse);

11: }

Nous constatons dans cet exemple que nous renvoyons la réponse bruitée attendue par le dispositif sondé lors d’une mesure, et tentons de respecter les contraintes temporelles par une temporisation de la durée de la programmation par SPI du périphérique émettant un signal (registres FTW0 dans l’implémentation matérielle) et recevant la réponse du dispositif sondé (readADC12()). Ce faisant, nous pouvons aussi profiter des tests unitaires sur la partie algorithmique du code, puisque cette partie ainsi que l’émulation des accès aux périphériques (mock pour nommer l’émulation de l’accès aux périphériques matériels) seront compilables sur PC.

Conclusion

Même si le coût du matériel pour s’approprier les techniques de développement sur système embarqué ne cesse de chuter, pour n’être plus que de quelques euros pour nombre de cartes d’évaluation, il est parfois souhaitable de tester une nouvelle architecture sans acquérir le matériel associé, ne serait-ce que pour les architectures encore difficiles à acquérir. Par ailleurs, nous avons montré comment un émulateur, en donnant accès au cœur du processeur, propose des fonctionnalités additionnelles aux debuggers logiciels ou matériels (JTAG), en permettant d’introduire des messages liés aux initialisations erronées de périphériques. Enfin, les émulateurs sont devenus tellement performants que même un système d’exploitation aussi complexe que GNU/Linux peut y être exécuté, par exemple pour analyser des logiciels fournis sous forme de binaire sur une plateforme exotique.

Remerciements

La référence bibliographique qui n'est pas librement disponible sur le Web a été acquise auprès de Library Genesis à http://gen.lib.rus.ec depuis le réseau universitaire Renater, une gare SNCF, au travers de TOR ou en exploitant comme DNS 9.9.9.9. Le tribunal qui s'est abaissé à légitimer l'attaque des éditeurs commerciaux contre cette ressource indispensable à nos activités quotidiennes de recherche et développement, en imposant de bloquer les DNS des fournisseurs d’accès internet, aurait pu se poser la question de l'applicabilité de sa décision.

Références

[1] GTVHACKER, « Hack All The Things – 20 Devices in 45 Minutes », DEFCON 22, 2014 : http://www.youtube.com/watch?v=u2aKrgDtfoI

[2] « ATmega16U4/ATmega32U4 8-bit Microcontroller with 16/32K bytes of ISP Flash and USB Controller » : http://ww1.microchip.com/downloads/en/devicedoc/atmel-7766-8-bit-avr-atmega16u4-32u4_datasheet.pdf version Atmel-7766J-USB-ATmega16U4/32U4-Datasheet 04/2016

[3] Q. MACÉ et J.-M FRIEDT, « FreeRTOS : application à la réalisation d’un analyseur de réseau numérique sur STM32 », GNU/Linux Magazine France n°207, 2017 : https://connect.ed-diamond.com/GNU-Linux-Magazine/GLMF-207/FreeRTOS-application-a-la-realisation-d-un-analyseur-de-reseau-numerique-sur-STM32

[4] RM0008 Reference Manual : http://www.st.com/resource/en/reference_manual/cd00171190.pdf

[5] G. GOAVEC-MEROU et J.-M. FRIEDT, « Un environnement exécutif visant la compatibilité POSIX : NuttX pour contrôler un analyseur de réseau à base de STM32 », GNU/Linux Magazine France 210, décembre 2017 : http://connect.ed-diamond.com/GNU-Linux-Magazine/GLMF-210/Un-environnement-executif-visant-la-compatibilite-POSIX-NuttX-pour-controler-un-analyseur-de-reseau-a-base-de-STM32

[6] D.A. PATTERSON et J.L. HENNESSY, « Computer organization and design – the hardware/software interface », RISC-V Edition, Elesevier-Morgan Kaufmann, 2018.

[7] R. BARRY, « FreeRTOS on RISC-V – Running the FreeRTOS kernel in RISC-V emulators and RISC-V hardware », FOSDEM 2019 RISC-V devroom : http://fosdem.org/2019/schedule/event/riscvfreertos/

[8] C. HEFFNER, « Exploiting Network Surveillance Cameras Like a Hollywood Hacker », Black Hat, 2013 : http://www.youtube.com/watch?v=B8DjTcANBx0 et le résumé disponible sur http://media.blackhat.com/us-13/US-13-Heffner-Exploiting-Network-Surveillance-Cameras-Like-A-Hollywood-Hacker-WP.pdf qui mentionne « Analysis and extraction of these firmware images was accomplished using Binwalk and the Firmware-Mod-Kit, while disassembly, emulation and debugging of code was performed using IDA Pro and Qemu. [...] It is worth noting that such tools allow attackers to identify and exploit vulnerabilities in embedded systems without ever purchasing a target device for testing. »



Article rédigé par

Par le(s) même(s) auteur(s)

Synchronisation d’ordinateurs par réseau informatique pour la datation sous GNU/Linux : NTP, PTP et GPS sur Raspberry Pi Compute Module 4

Magazine
Marque
Hackable
Numéro
51
Mois de parution
novembre 2023
Spécialité(s)
Résumé

Nombre d’outils, à commencer par make, s’appuient sur la date d’accès aux fichiers pour décider de leur obsolescence. Dans le cadre d’intercomparaisons d’horloges, nous effectuons des acquisitions par radio logicielle sur divers sites géographiquement distincts et nous nous interrogeons sur la date d’acquisition avec une résolution aussi élevée que possible. Que veut dire « élevée » et quel niveau de synchronisation pouvons-nous espérer entre deux ordinateurs exécutant GNU/Linux ? Nous conclurons avec la nécessité de corriger l’erreur de l’oscillateur qui cadence le processeur et démontrerons comment quelques composants passifs sur Compute Module 4 permettent d’atteindre ce résultat.

Du domaine temporel au domaine spectral dans 2,5 kB de mémoire : transformée de Fourier rapide sur Atmega32U4 et quelques subtilités du C

Magazine
Marque
Hackable
Numéro
49
Mois de parution
juillet 2023
Spécialité(s)
Résumé

Nous avons exploré diverses implémentations libres de transformées de Fourier discrètes rapides (FFT), mais leur occupation en mémoire reste de la dizaine de kilooctets. Que peut-on faire avec 2,5 kB de mémoire ? La vénérable note d’application 3722 de Maxim IC nous enseigne comment implémenter efficacement une FFT sur microcontrôleur 8-bits et l’arithmétique en virgule fixe, et la notation en complément à deux au passage.

FreeRTOS dans 2,5 KB de RAM sur Atmega32U4

Magazine
Marque
Hackable
Numéro
48
Mois de parution
mai 2023
Spécialité(s)
Résumé

FreeRTOS [1], l’environnement exécutif de Richard Barry plébiscité par Amazon Web Services (AWS), fonctionne sur une plateforme matérielle ou son émulateur munis de seulement 2,5 KB de RAM. La mise en œuvre de FreeRTOS dans aussi peu de ressources fournit une opportunité de plonger dans les détails de l’implémentation de ses fonctions.

Les derniers articles Premiums

Les derniers articles Premium

Quarkus : applications Java pour conteneurs

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Initié par Red Hat, il y a quelques années le projet Quarkus a pris son envol et en est désormais à sa troisième version majeure. Il propose un cadre d’exécution pour une application de Java radicalement différente, où son exécution ultra optimisée en fait un parfait candidat pour le déploiement sur des conteneurs tels que ceux de Docker ou Podman. Quarkus va même encore plus loin, en permettant de transformer l’application Java en un exécutable natif ! Voici une rapide introduction, par la pratique, à cet incroyable framework, qui nous offrira l’opportunité d’illustrer également sa facilité de prise en main.

De la scytale au bit quantique : l’avenir de la cryptographie

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Imaginez un monde où nos données seraient aussi insaisissables que le célèbre chat de Schrödinger : à la fois sécurisées et non sécurisées jusqu'à ce qu'un cryptographe quantique décide d’y jeter un œil. Cet article nous emmène dans les méandres de la cryptographie quantique, où la physique quantique n'est pas seulement une affaire de laboratoires, mais la clé d'un futur numérique très sécurisé. Entre principes quantiques mystérieux, défis techniques, et applications pratiques, nous allons découvrir comment cette technologie s'apprête à encoder nos données dans une dimension où même les meilleurs cryptographes n’y pourraient rien faire.

Les nouvelles menaces liées à l’intelligence artificielle

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Sommes-nous proches de la singularité technologique ? Peu probable. Même si l’intelligence artificielle a fait un bond ces dernières années (elle est étudiée depuis des dizaines d’années), nous sommes loin d’en perdre le contrôle. Et pourtant, une partie de l’utilisation de l’intelligence artificielle échappe aux analystes. Eh oui ! Comme tout système, elle est utilisée par des acteurs malveillants essayant d’en tirer profit pécuniairement. Cet article met en exergue quelques-unes des applications de l’intelligence artificielle par des acteurs malveillants et décrit succinctement comment parer à leurs attaques.

Les listes de lecture

9 article(s) - ajoutée le 01/07/2020
Vous désirez apprendre le langage Python, mais ne savez pas trop par où commencer ? Cette liste de lecture vous permettra de faire vos premiers pas en découvrant l'écosystème de Python et en écrivant de petits scripts.
11 article(s) - ajoutée le 01/07/2020
La base de tout programme effectuant une tâche un tant soit peu complexe est un algorithme, une méthode permettant de manipuler des données pour obtenir un résultat attendu. Dans cette liste, vous pourrez découvrir quelques spécimens d'algorithmes.
10 article(s) - ajoutée le 01/07/2020
À quoi bon se targuer de posséder des pétaoctets de données si l'on est incapable d'analyser ces dernières ? Cette liste vous aidera à "faire parler" vos données.
Voir les 64 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous