Mesure de latences d’interruptions avec un STM32

Publié par cpb
Mar 13 2013

Raspberry Pi - STM32 - 01J’ai continué mes expériences avec la gestion d’interruptions par un driver RTDM-Xenomai sur les ports GPIO d’un Raspberry Pi que j’avais entamées dans le précédent article. Jusqu’alors j’avais pu déterminer « à l’œil » en utilisant un oscilloscope que le temps de réponse était de l’ordre de 3 à 4 micro-secondes, avec des pointes régulières à 8 micro-secondes environ. Ce sont justement ces pointes – et celles plus longues – qui m’intéressent et j’aimerais en avoir un aperçu plus complet. Mon objectif est de pouvoir déterminer le temps de réponse que peut garantir dans le pire des cas un système temps réel construit sur Xenomai avec l’API RTDM.

Ceci pourrait être réalisé avec un oscilloscope numérique ou un analyseur logique à mémoire. Toutefois, ces instruments ne mémorisent généralement qu’un nombre assez faible (quelques centaines ou milliers) de valeurs. J’aimerais avoir des statistiques sur plusieurs centaines de milliers, voire millions d’échantillons. En outre, je souhaite disposer éventuellement d’éléments statistiques plus complets (histogramme des latences, etc.).

J’ai donc décidé de me construire un petit outil de mesure en utilisant un micro-contrôleur externe au Raspberry Pi. Ce micro-contrôleur devra envoyer un signal de déclenchement d’interruption et mesurer le plus précisément possible le temps s’écoulant avant la réponse du Raspberry Pi. L’intérêt d’utiliser un micro-contrôleur est que ce type de matériel est programmé aisément pour réaliser une seule tâche, sans perturbation externe, à l’inverse des micro-processeurs sur lesquels on fait tourner un système d’exploitation qui peut être « parasité » par des interruptions imprévues (réseau, vidéo, etc.).

J’ai choisi un micro-contrôleur assez puissant et peu coûteux : le STM 32. L’implémentation que j’ai décidé d’utiliser est très simple (et très bon marché) : il s’agit de la carte STM32-H103 d’Olimex (on pourrait parfaitement réaliser la même expérience avec d’autres cartes de développement).

Ce petit micro-contrôleur est doté de plusieurs timers (dont le nombre varie suivant la catégorie du micro-contrôleur) capables notamment de fonctionner dans un mode appelé « Input Capture » qui m’intéressera ici. Le timer « tourne » en permanence en comptant des cycles d’horloge, et lorsqu’un signal d’entrée spécifique se présente, il mémorise l’état du compteur dans un registre avant d’appeler un handler d’interruption. Ici j’utiliserai deux signaux d’entrée différents. Observons le fonctionnement sur la capture d’oscilloscope suivante.

Réponse aux interruptions du Raspberry Pi

Au point 1, le micro-contrôleur bascule l’état d’une sortie (signal du haut), qui est connectée à l’entrée d’interruption du Raspberry Pi. Ce même signal est rebouclé en entrée du premier canal du timer 1 du STM32. Le mode Input Capture mémorise la valeur du compteur et appelle une interruption du micro-contrôleur. Nous enregistrons alors cette valeur de compteur.

Sur le Raspberry Pi, une interruption a également été déclenchée, et elle a fait basculer l’état d’un GPIO de sortie (au point 2 du graphique). Celui-ci est relié à une entrée du STM32, sur le second canal du timer 1. À nouveau le timer mémorise la valeur de son compteur et appelle notre gestionnaire d’interruption qui en déduit la durée du traitement sur le Raspberry Pi. une fois cette latence mesurée, le STM32 peut faire redescendre son signal (au point 3),  et reprendre le même travail après avoir envoyé la valeur mesurée sur son port RS-232.

Raspberry Pi - STM32 - 02

Le branchement réalisé est schématisé sur la figure suivante.

Raspberry-Pi & STM-32

Le code de mon programme employé sur le STM 32 est le suivant.

/****************************************************************************\
 * Mesure de temps de reponse a un signal                                   *
 *                                                                          *
 * Christophe Blaess 2013.                                                  *
\****************************************************************************/

#include <stdarg.h>
#include <stdio.h>
#include <string.h>

#include <libopencm3/cm3/nvic.h>
#include <libopencm3/cm3/systick.h>
#include <libopencm3/stm32/timer.h>
#include <libopencm3/stm32/usart.h>
#include <libopencm3/stm32/f1/rcc.h>   // Reset and Clock Control.
#include <libopencm3/stm32/f1/gpio.h>

// ---------------------------------------------------------------------------
// 
void clock_setup(void)
{
	// STM32-H103 use an external quartz at 8MHz
	rcc_clock_setup_in_hse_8mhz_out_72mhz();

	// Enable clock for GPIO ports A & C.
	rcc_peripheral_enable_clock(& RCC_APB2ENR, RCC_APB2ENR_IOPAEN);
	rcc_peripheral_enable_clock(& RCC_APB2ENR, RCC_APB2ENR_IOPCEN);

}

// ---------------------------------------------------------------------------
// 
void usart_setup(void)
{
	rcc_peripheral_enable_clock(& RCC_APB1ENR, RCC_APB1ENR_USART2EN);
	rcc_peripheral_enable_clock(& RCC_APB2ENR, RCC_APB2ENR_IOPAEN);
	gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ,
	              GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, GPIO_USART2_TX);
	gpio_set_mode(GPIOA, GPIO_MODE_INPUT,
	              GPIO_CNF_INPUT_FLOAT, GPIO_USART2_RX);
	usart_set_baudrate (USART2, 115200);
	usart_set_databits (USART2, 8);
	usart_set_stopbits (USART2, USART_STOPBITS_1);
	usart_set_parity   (USART2, USART_PARITY_NONE);
	usart_set_flow_control (USART2, USART_FLOWCONTROL_NONE);
	usart_set_mode (USART2, USART_MODE_TX_RX);
	usart_enable (USART2);
}

int usart_printf(const char * format, ...)
{
	va_list args;
	int r;
	int i;
	char buffer[256];

	if (format == NULL) 
		return 0;

	va_start(args, format);
	r = vsnprintf(buffer, 256, format, args);
	va_end(args);
	if (r <= 0)
		return r;

	for (i = 0; buffer[i] != '\0'; i ++)
		usart_send_blocking(USART2, buffer[i]);
	return i;
}

// ---------------------------------------------------------------------------
//

static int systick_counter_ms = 0;

static void systick_setup()
{
	// 72 MHz with DIV 8 -> systick clock source of 9MHz
	systick_set_clocksource(STK_CTRL_CLKSOURCE_AHB_DIV8);

	// Systick interrupt period 1ms -> 9MHz / 1kHz = 9000
	systick_set_reload(9000 - 1);
	systick_interrupt_enable();
	systick_counter_enable();
}

void sys_tick_handler(void)
{
	systick_counter_ms ++;
}

// ---------------------------------------------------------------------------
//
void tim1_setup_input_capture()
{
	// Enable clock for Timer 1.
	rcc_peripheral_enable_clock(& RCC_APB2ENR,
	                            RCC_APB2ENR_TIM1EN);

	// Configure TIM1_CH1 and TIM1_CH2 as inputs.
	gpio_set_mode (GPIO_BANK_TIM1_CH1, GPIO_MODE_INPUT,
	               GPIO_CNF_INPUT_PULL_UPDOWN,
	               GPIO_TIM1_CH1);

	gpio_set_mode (GPIO_BANK_TIM1_CH2, GPIO_MODE_INPUT,
	               GPIO_CNF_INPUT_PULL_UPDOWN,
	               GPIO_TIM1_CH2);

	// Enable interrupts for TIM1 CC.	
	nvic_enable_irq        (NVIC_TIM1_CC_IRQ);

	timer_set_mode(TIM1,
	               TIM_CR1_CKD_CK_INT, // Internal 72 MHz clock
	               TIM_CR1_CMS_EDGE,   // Edge synchronization
	               TIM_CR1_DIR_UP);    // Upward counter

	timer_set_prescaler     (TIM1, 72-1);  // Counter unit = 1 us.
	timer_set_period        (TIM1, 0xFFFF);
	timer_set_repetition_counter(TIM1, 0);
	timer_continuous_mode   (TIM1);

	// Configure channel 1
	timer_ic_set_input     (TIM1, TIM_IC1, TIM_IC_IN_TI1);
	timer_ic_set_filter    (TIM1, TIM_IC1, TIM_IC_OFF);
	timer_ic_set_polarity  (TIM1, TIM_IC1, TIM_IC_RISING);
	timer_ic_set_prescaler (TIM1, TIM_IC1, TIM_IC_PSC_OFF);
	timer_ic_enable        (TIM1, TIM_IC1);
	timer_clear_flag       (TIM1, TIM_SR_CC1IF);
	timer_enable_irq       (TIM1, TIM_DIER_CC1IE);

	// Configure channel 2
	timer_ic_set_input     (TIM1, TIM_IC2, TIM_IC_IN_TI2);
	timer_ic_set_filter    (TIM1, TIM_IC2, TIM_IC_OFF);
	timer_ic_set_polarity  (TIM1, TIM_IC2, TIM_IC_RISING);
	timer_ic_set_prescaler (TIM1, TIM_IC2, TIM_IC_PSC_OFF);
	timer_ic_enable        (TIM1, TIM_IC2);
	timer_clear_flag       (TIM1, TIM_SR_CC2IF);
	timer_enable_irq       (TIM1, TIM_DIER_CC2IE);

	timer_enable_counter   (TIM1);

}

volatile int latency = 0;
volatile int latency_max = 0;

void tim1_cc_isr(void)
{
	static int last_value = 0;
	int value;
	if (timer_get_flag(TIM1, TIM_SR_CC1IF) != 0) {
		// Timer channel 1 interrupt -> First edge (outcoming signal).
		timer_clear_flag(TIM1, TIM_SR_CC1IF);
		last_value = timer_get_counter(TIM1);
	}
	if (timer_get_flag(TIM1, TIM_SR_CC2IF)) {
		// Timer channel 2 interrupt -> response (incoming signal).
		timer_clear_flag(TIM1, TIM_SR_CC2IF);
		value = timer_get_counter(TIM1) - last_value;
		if (value < 0)
			value = value + 0xffff;
		latency = value;
		if (latency > latency_max)
			latency_max = latency;
	}
}

// ---------------------------------------------------------------------------
//
int main(void)
{
	clock_setup();
	usart_setup();
	systick_setup();
	tim1_setup_input_capture();

	// Wait 3 seconds to allow Linux detecting /dev/ttyUSB
	while (systick_counter_ms < 3000)
		;

	// Output signal is on GPIO C 10 (EXT1-8)
	gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_2_MHZ,
	              GPIO_CNF_OUTPUT_PUSHPULL, GPIO10);

	while (1) {
		// If the input signal is high.
		if (gpio_get(GPIO_BANK_TIM1_CH2, GPIO_TIM1_CH2) != 0) {
			// Output a raising edge.
			gpio_set(GPIOC, GPIO10);
			// Wait for the input signal going down, max 2 ms.
			for (systick_counter_ms = 0; systick_counter_ms < 2; )
				if (gpio_get(GPIO_BANK_TIM1_CH2, GPIO_TIM1_CH2) == 0)
					break;
			// Output a falling edge.
			gpio_clear(GPIOC, GPIO10);
		} else {
			latency = 0;
			// Output a raising edge (trigger signal).
			gpio_set(GPIOC, GPIO10);
			// Wait for the latency to be available.
			for (systick_counter_ms = 0; systick_counter_ms < 2; )
				if (latency != 0)
					break;
			// Output a falling edge.
			gpio_clear(GPIOC, GPIO10);
			if (latency != 0)
				usart_printf("%3d - %3d\n", latency, latency_max);
		}

		// Wait 1 ms before next trigger.
		for (systick_counter_ms = 0; systick_counter_ms < 1; )
			;
	}
	return 0;
}

Sur le Rapsberry Pi, le programme employé ici est le driver pour RTDM dont nous avons parlé dans cet article.

Sur le PC Linux, relié au port série du STM32, nous pouvons voir défiler les latences mesurées (en micro-secondes) ainsi que la latence maximale depuis le démarrage du STM32.

$ sudo cat /dev/ttyUSB0
 12 -  31
  9 -  31
  8 -  31
  9 -  31
  8 -  31
  9 -  31
  7 -  31
  4 -  31
  4 -  31
  4 -  31
  4 -  31
  9 -  31
  8 -  31
  8 -  31
  9 -  31
 11 -  31
 11 -  31
  9 -  31
  8 -  31
  9 -  31
 12 -  31
 11 -  31
  8 -  31

L’intérêt de cette expérience est de connaître la valeur maximale que nous risquons d’atteindre. Aussi est-il indispensable de charger fortement le Raspberry Pi. C’est le rôle du script dohell livré avec Xenomai, et que je fais tourner en séquences régulières de cinq minutes entrecoupées de petits repos de trente secondes.

# while true; do /usr/xenomai/bin/dohell 300; sleep 30; done

Résultats

La première expérience consiste à faire une série de mesures pendant une heure, avec un Raspberry Pi peu chargé (quelques accès au système de fichiers et consultations d’information de /proc). Les résultats bruts se trouvent dans ce fichier. La latence maximale obtenue est de 24 microsecondes.

J’ai utilisé les petits outils d’analyse des résultats que j’avais développés pour mon livre « Solutions temps réel sous Linux » afin d’obtenir un histogramme des latences. L’axe des ordonnées est logarithmique, afin de mettre en évidence les cas les plus rares (au dessus de 15 microsecondes environ).

Latences Xenomai Raspberry Pi - Faible charge

L’expérience suivante a consisté à laisser le système faire des mesures pendant vingt-quatre heures, avec une très forte charge système sur le Raspberry Pi. Pour cela j’ai lancé le script dohell mentionné plus haut. Les résultats bruts se trouvent dans ce fichier. La latence maximale rencontrée est de 46 micro-secondes. L’histogramme montre bien les répartitions des latences mesurées. Nous y retrouvons le premier pic centré sur 3-4 microsecondes obtenues pendant les périodes de repos entre l’exécution de dohell.

Latences Xenomai Raspberry Pi - 24h - Forte chargeNous voyons que la latence de traitement d’une interruption sur Raspberry Pi en utilisant l’API RTDM de Xenomai est inférieure à 50 microsecondes, la valeur maximale étant par ailleurs un cas isolé et très rare. Ces résultats sont très corrects pour un système embarqué peu coûteux et – rappelons-le – non prévu pour un usage industriel.

Conclusion

Pour valider un choix technologique ou vérifier la montée en charge d’un prototype, il est souvent nécessaire de réaliser ce type d’expérience. On voit qu’il est tout à fait possible de construire aisément ses propres outils de mesure adaptés, pour un prix de revient modique et une complexité relativement faible (le plus compliqué est la lecture et l’interprétation du manuel de référence du micro-contrôleur). Je vous encourage à utiliser ces petites plates-formes souples, performantes et bon marché que sont le Raspberry Pi, le STM32, etc. pour vos propres hacks.

Comme toujours, les remarques, commentaires et retours d’expériences sont les bienvenus.

15 Réponses

  1. Tomaok dit :

    Bonjour,

    Merci pour cet article, c’est une mesure intéressante.
    Je me pose juste une question d’ordre générale, que deviendrait ce type de mesure sur un système multi-coeur ?
    Autrement dit, peut on tirer profit d’un système multi-coeur pour la gestion d’interruptions?

    • cpb dit :

      Bonjour,

      Pour un driver Linux classique il est assez facile de tirer parti du multi-cœur en redirigeant le traitement d’une interruption vers un CPU donné (en modifiant le masque dans /proc/irq/<numero-irq>/smp_affinity) et en redirigeant vers les autres CPU les autres interruptions (ainsi que les tâches courantes avec la commande taskset ou les cgroups). Ceci permet de protéger un code critique contre les interruptions ou au contraire de privilégier le traitement d’une IRQ quelque soit l’activité du système et les interruptions déjà en cours de traitement.

      Pour un driver Xenomai je ne crois pas que cela soit possible via l’API RTDM, il faut utiliser une fonction bas-niveau implémentée dans ipipe : rthal_set_irq_affinity(irq, affinity). Je ne l’ai jamais testée, je ferai un petit essai sur Pandaboard à l’occasion.

  2. Bonjour,
    Je n’avais pas compris toute de suite pourquoi lire sur l’interruption de l’entrée rebouclée la valeur du compteur plutôt que lire la valeur du compteur avant de positionner la sortie. Comme vous le dites l’enregistrement est « matériel » : l’IT n’est que pour récupérer la capture. Donc pas de problème de latence liée au saut dans l’ISR, de plus on se positionne au moment où l’état de la patte correspond à l’état logique.
    Dans la boucle main(), le code qui correspond à « If the input signal is high… » c’est bien pour tenter de remettre l’interface à l’état bas et empêcher le lancement d’une acquisition ?
    Est-ce que l’on ne pouvait pas envisager le même programme sur la raspberry, ie du code directement exécuté à partir d’uboot ?
    Avez-vous des ordres de grandeur à donner pour les cartes à « usages industriels », avec Xenomai ?

    Je ne connais pas Xenomai, mais aurait-il été possible de donner une plus forte priorité aux IT des GPIO plutôt qu’au IT liés au systèmes (exemple : tick de l’ordonnanceur), de sorte à créer un temps de latence indépendant de la charge CPU ?

    Merci pour ces petits essais ça donne bien envie de s’y mettre.

  3. Guy dit :

    merci Christophe pour tous ces essais intéressants. Le sujet est passionnant et a travers vos articles ont comprend tout le chemin qui a été fait dans l’electronique avec des produits aussi performant et aussi peu couteux que le Raspberry Pi. Est il possible d’optimiser encore plus le temps de réponse aux interruptions en supprimant les tâches du noyau inutiles a une application industrielle, ex pas besoin de vidéo, d’usb… En gardant la couche réseau. En fait comme vous le dites bien en comparant un microcontrôleur monotâche et un OS.
    Avez vous pensé a proposer ces articles au MagPi? Je pense qu’ils y ont leur place.
    Merci encore

    Guy

  4. Pierre Gradot dit :

    Je voulais juste dire que je trouve cet article magnifique 🙂

  5. FabienT dit :

    Bonjour,

    Je develope beaucoup en java, j’essai de faire un peu d’électronique le week (en assembleur sur des pics). Récemment un truc me trotte dans la tete : essayer de driver des WS2811 sur avec un raspberry. Sur un pic on y arrive en utilisant de façon détournée l’I2C. Le WS2811 utilise un cablage sans horloge et demande une précision sur le timing de +-150ns sur des durées de 0,5us à 2us. Le driver I2C qui mount le port dans le filesystem n’est malheureusement pas utilisable car il semble y avoir un delais entre l’envoi des différents octets.

    J’ai essayé de lire tout vos articles, et si j’ai bien comprit :
    -Meme avec un patch temps réel je n’aurais pas la précision suffisante pour le faire en multi tache
    -Avec un programme en mode kernel, bit-banger (mais pour controller 128 leds a 24 images par seconde, je vais bloquer 10% du temps CPU disponible)
    -Un autre possibilité serait peut etre d’aller fouiller dans le code du driver ( http://searchcode.com/codesearch/view/26436326 ?) et essayer de comprendre comment utiliser les fonction I2C du cheap sans latence entre les octets si c’est possible.

    La question : par ou il faudrait que je commence ?

    Merci,
    Fabien.

    PS : oui, il y a la solution tres simple de programmer un pic qui communique en I2C avec le raspberry, bufferise, et implément le protocol du WS2811, mais ca je sais deja faire, c’est pas drole.

    • cpb dit :

      Bonjour,

      Il me paraît peu probable d’arriver en espace utilisateur à des latences inférieures à 150 ns pour des périodes de l’ordre de la microseconde. Surtout sur un Raspberry Pi.

      L’étude et l’adaptation du driver pour l’i2c du BCM2708 me semble une bonne idée, mais après un coup d’oeil rapide, je n’ai pas vu où peuvent se glisser les latences entre les octets. Peut-être faut-il essayer de regrouper plus de traitement au niveau du handler d’interruption sans passer par le mécanisme des wait_for_completion()/complete(). Ou du moins remplacer le traitement par completions en spinlock.

      Dédier une partie du travail à un petit micro-contrôleur, du type STM32 avec qui on communique en SPI peut aussi être une solution.

      Bon courage !

  6. Tomasz dit :

    Welcome

    I have a big request:
    you have a working code for STM32 i2C in libopenCM3?
    I get tired already been nine days since my microcontroller hangs on:
    while (! (I2C_SR1 (I2C) & (I2C_SR1_TxE))) / / wait for DR empty

    Can you help me?

  7. Serign dit :

    Bonjour Christophe,
    Merci beaucoup pour vos articles intéressants. Je suis un de vous fans:J’ai tous vous livres dans mon bureau.
    Vous avez declaré la fonciton void sys_tick_handler(void) dans le programme que vous avez employé sur le STM32 mais la function n’ est pas utilsé (appelé) dans le program. Pouvez vous m’expliqué son role?
    Merci

    • cpb dit :

      Bonjour,
      Cette fonction est appelée automatiquement à chaque tick système, son nom est prédéfini, il n’est pas nécessaire de l’enregistrer ou de l’appeler explicitement. En l’occurrence elle sert à incrémenter un compteur de millisecondes.

      Il y a souvent des fonctions aux noms prédéfinis dans la programmation sur micro-contrôleur, par exemple nous avons aussi tim1_cc_isr() définie un peu plus bas qui est invoquée automatiquement lors du déclenchement de l’interruption du timer 1.

  8. Serign dit :

    Merci beaucoup,
    J’ai beaucoup apris grace a vos articles.
    Que dieu vous benit!

  9. gch dit :

    Bonjour,

    juste une petite remarque, le test dohell, lancé sans argument crée une charge, mais
    pas vraiment suffisante pour espérer s’approcher du cas le pire. Voir dohell –help pour
    une liste de charges possible. J’utilise:

    dohell -s 192.168.0.5 -m /mnt -l /ltp

    avec une clé USB montée sur /mnt, et LTP installé dans /ltp

    Cordialement.

  10. Mouha dit :

    Bonjour,

    Je souhaiterai faire la même chose avec une BeagleBone Black: mesurer les performances de cette carte.
    Sachant que :
    -celle-ci est maintenant intégrée officiellement ( http://git.xenomai.org/xenomai-2.6.git/tree/ksrc/arch/arm/patches/README )
    – je possède votre livre également (p263 notamment)

    En l’état d’avancée actuel, d’après-vous existe-t-il un moyen simple -prendre des mesures- pour ce faire svp ?

  11. Nicanord dit :

    Bonjour Monsieur,

    Je suis étudiant et je en train de tester les performance RT linux avec patch rt. je désire calculer la latence entre l’arrivée d’une irq et la prise en charge par le handler. Pour Xenomai, il existe « latency » pour le calculer. Il y a t-il un outil qui permettrai de le faire egalement sous Preemt-rt? si la réponse est non, pouvez-vous me donner quelques conseil afin que je créer un petit programme dans ce sens.

    Merci

URL de trackback pour cette page