En 2019, STMicroelectronics sort une nouvelle gamme de produits pour s'ouvrir au marché des processeurs applicatifs.
La gamme STM32MP1 est une famille de processeurs ARM applicatifs conçus par STMicroelectronics qui regroupe deux catégories de SoC :
- Les STM32MP13 qui possèdent un processeur ARM Cortex-A7
- Les STM32MP15 qui possèdent un processeur ARM Cortex-A7 et un coprocesseur ARM Cortex-M4
Cet article fait suite à un travail exploratoire du STM32MP157F-EV1. L'un des objectifs était de mettre en place une communication inter-processeurs entre le cœur Cortex-A7 et le cœur Cortex-M4.
Dans cet article nous détaillerons le fonctionnement et la mise en place d'une communication inter-processeurs sur la gamme STM32MP15 ainsi que certaines de ses limites.
Fonctionnement de la communication inter-processeurs
Un élément central de la communication inter-processeurs conçue par STMicroelectronics est un composant appelé IPCC.
L’IPCC pour Inter Processor Communication Control (Contrôle de communication inter processeurs en français) est un périphérique d’interruption servant à signaler la lecture et l’écriture des données échangées par les deux processeurs.
Il se représente sous la forme de canaux bidirectionnels, chacun composé de 2 sous-canaux unidirectionnels et de sens opposés. Les STM32MP15 possèdent 6 canaux bidirectionnels.

L'appellation de canal est une abstraction et peut porter à confusion. Retenez qu'aucune donnée ne transite à travers. En réalité, derrière ce concept de "canal" IPCC se cache un jeu de registres déclenchant des interruptions. Ces interruptions sont ensuite traitées par les processeurs et interprétées pour lire ou écrire en mémoire partagée.
Nous verrons le fonctionnement des registres dans la suite de cet article.
Un second fait intéressant sur ce schéma est que nous avons deux types d’interruptions :
- RXO pour Receive channel Occupied
- TXF pour Transmit channel Free
Le premier sert à signaler au processeur local qu’un message émis par le processeur distant a été reçu. Le second permet de signaler au processeur local que le processeur distant a traité le message et qu’il est possible d’en envoyer un nouveau.
Voici un exemple du fonctionnement :
Si P1 envoie un message à P2, il commence par écrire les données en mémoire partagée puis demande à l’IPCC de déclencher l’interruption de réception de P2 via le canal 1 (RXO IRQ du sous-canal P1→P2). P2 reçoit l’interruption et sait qu’il a un message en attente. Une fois le message lu et traité, P2 demande à l’IPCC de déclencher l’interruption de fin de transmission de P1 via le canal 1 (TXF IRQ du sous-canal P1→P2).
Dans ce contexte, le fait d’utiliser le canal 1 pour signaler la communication est prédéfini par le développeur. On aurait très bien pu utiliser un autre canal, tant que cela est connu à l’avance par les deux processeurs.
Ceci étant dit, comment est-ce que l’on demande à l’IPCC de déclencher une interruption ?
Comme indiqué plus haut, l’IPCC est un regroupement de plusieurs bancs de registres. Pour rester compréhensible nous n’allons nous attarder que sur trois d’entre eux, mais sachez qu’il en existe d’autres, notamment des registres servant de masque d’interruption.
Chaque processeur possède un banc complet de registres. C1 caractérisera le groupe de registre utilisé par P1 et C2 celui utilisé par P2 (ces appellations sont celles utilisées par ST). Pour simplifier, nous allons nous concentrer sur le cas de la communication de P1 vers P2. Il suffit d'inverser l'utilisation de C1 et C2 pour avoir la communication dans le sens inverse.
C1.CHnF, pour CHannel n status Flag, est un banc de registre d’état en lecture seule. Lorsqu’un registre passe de 0 à 1, il déclenche l’interruption de réception occupée (RXO) de P2, et quand un registre passe de 1 à 0, il déclenche l’interruption de transmission libre (TXF) de P1. Quand le processeur reçoit une interruption, il vient regarder l’état des registres et lance les callbacks rattachés aux canaux correspondants.
C1.CHnS et C2.CHnC respectivement pour CHannel n status Set et CHannel n status Clear permettent de modifier l’état à 1 ou 0 du registre C1.CHnF correspondant lorsqu’on écrit la valeur 1 dedans.
Voici un exemple :

Détaillons chacune des étapes :
- Étape 0 : P1 écrit un message à destination de P2 dans un espace mémoire partagé.
- Étape 1 : P1 écrit dans le registre C1.CH1S.
- Étape 2 : La valeur de C1.CH1F est automatiquement fixée à 1 (set)
- Étape 3 : Ce qui déclenche l’interruption RXO de P2 signalant qu’un message a été reçu.
- Étape 4 : P2 lit le message dans l'espace mémoire partagé.
- Étape 5 : P2 écrit dans le registre C2.CH1C.
- Étape 6 : La valeur de C1.CH1F est automatiquement fixée à 0 (clear)
- Étape 7 : Déclenchement de l’interruption TXF de P1 signalant que P2 a traité le message et qu’un nouveau peut être envoyé.
Nous savons comment avertir qu’un message a été envoyé et qu’il a été traité. Mais comment pouvons nous lire et écrire un message ?
Pour cela, STMicroelectronics nous propose d'utiliser le framework RPMSG côté A7 et le framework OpenAMP côté M4.

RPMSG est un bus de messagerie permettant d’écrire et de lire en mémoire partagée. Il fonctionne avec un point d’entrée/sortie (par exemple ttyRPMSG) avec lequel l’utilisateur interagit.
OpenAMP est un framework implémentant, pour du bare metal ou du RTOS, les fonctionnalités de certains drivers Linux dont RPMSG.
OpenAMP et RPMSG viennent écrire et lire à un endroit prédéfini en mémoire partagée. L’IPCC nous sert alors à ordonner ces actions.
Exemple d’utilisation de la communication inter-processeur
Dans cette partie, nous allons parcourir un exemple d'application ping-pong utilisant la communication inter-processeurs. Nous utilisons la carte d’évaluation STM32MP157F-EV1 de ST, mais cet exemple devrait être réalisable avec n'importe quel modèle STM32MP15. Vous retrouverez les sources de cet exemple dans le dépôt Git suivant : https://github.com/Openwide-Ingenierie/demo-article-inter-proc.git
Le répertoire contient un "sous-module". Pour le cloner, il vous faudra utiliser la commande suivante :
git clone --recurse-submodules \
https://git.smile.fr/demonstrateur-stm32mp1/demo-article-inter-proc.git
Le dossier STM32CubeMP1 (ressources fournies par STMicroelectronics) est un peu volumineux, l’opération peut prendre plus ou moins de temps en fonction de votre débit.
Pour pouvoir mettre en place cet exemple, il est nécessaire d’avoir un Linux sur la carte. Pour cela, vous pouvez suivre la documentation de STMicroelectronics : https://wiki.st.com/stm32mpu/wiki/Getting_started/STM32MP1_boards/STM32MP157x-EV1/Let%27s_start
Nous allons développer uniquement la partie microcontrôleur (Cortex-M4) car côté Linux (Cortex-A7) nous allons utiliser le driver ttyRPMSG permettant à l’utilisateur d’envoyer et de lire des messages échangés avec le coprocesseur. Ce driver a été développé et contribué "upstream" dans le noyau Linux par STMicroelectronics .
La majeure partie de ce tutoriel utilisera un tty côté Linux pour faire l'interface de la communication. Assurez-vous que votre noyau Linux ait été compilé avec l'option CONFIG_TTY=y.
La partie 4 de ce tutoriel implique d'avoir compilé le noyau Linux avec les options CONFIG_RPMSG_CHAR=y et CONFIG_RPMSG_CTRL=y.
Nous partons également du principe que nous n’utilisons pas le CubeIDE de STMicroelectronics. Cela va nous rajouter une étape supplémentaire pour compiler le projet.
Il vous faudra donc installer la toolchain arm-none-eabi 13.2.Rel1 ou plus pour pouvoir faire de la compilation croisée vers la carte : https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads
Vous pouvez soit renseigner le lieu d'installation dans la variable d'environnement PATH soit en ajoutant la variable GCC_PATH lorsqu'il vous sera demandé de compiler le code source.
1) Description du contenu
Commençons par regarder brièvement ce qu’il y a dans le répertoire que vous venez d'installer.
Le dossier STM32CubeMP1 contient la HAL et autres interfaces d'abstraction nous permettant de manipuler les registres accessibles par le Cortex-M4 ainsi que des "middlewares third party" (FreeRTOS et OpenAMP).
Le dossier src contient nos sources et le dossier inc contient nos en-têtes.
2) La communication inter-processeur
Le répertoire Git possède plusieurs tags correspondant aux différentes parties de cet article. Je vous invite donc à basculer sur le tag "basic-rpmsg".
git switch --detach basic-rpmsg
Comme indiqué précédemment, pour pouvoir utiliser la communication inter-processeurs sur le Cortex-M4, il nous faut utiliser OpenAMP. Par chance, STMicroelectronics nous fournit des templates pour le configurer.
Les fichiers listés ci-dessous sont issus du répertoire STM32CubeMP1/Middlewares/Third_Party/OpenAMP/mw_if/ :
- openamp_conf est le fichier de configuration d’OpenAMP, on y défini entre autre le mode de fonctionnement de la mailbox.
- rsc_table est un ensemble de valeurs et de structures définissant les ressources disponible pour OpenAMP.
- openamp initialise et implémente une interface pour OpenAMP.
- mbox_ipcc implémente une interface utilisée par openamp faisant le lien entre OpenAMP et les interruptions IPCC. Nous reviendrons en détail dessus.
Par exemple, comme nous utilisons le template mbox_ipcc, nous avons décommenté les lignes 38 et 82 du fichier inc/openamp_conf.h. Cela nous permet d’inclure l’utilisation de mbox_ipcc dans openamp.
...
#define MAILBOX_IPCC_IF_ENABLED
...
#define LINUX_RPROC_MASTER
L'initialisation et la gestion des interruptions IPCC se font dans les fichier src/ipcc.c et src/stm32mp1xx_it.c. La mise en place est similaire aux autres types d'interruptions chez STMicroelectronics.
La variable hipcc (définie dans main.c et initialisée dans les deux fichiers précédents) va gérer toutes les lignes d'interruption d'IPCC, ce qui revient dans notre cas à 6 lignes de réceptions et 6 lignes de transmission.
IPCC_HandleTypeDef hipcc;
On retrouve la définition de ce type dans le fichier STM32CubeMP1/Drivers/STM32MP1xx_HAL_Driver/Inc/stm32mp1xx_hal_ipcc.h :
typedef struct __IPCC_HandleTypeDef
{
IPCC_TypeDef *Instance; /*!< IPCC registers base address */
void (* ChannelCallbackRx[IPCC_CHANNEL_NUMBER])(struct __IPCC_HandleTypeDef *hipcc, uint32_t ChannelIndex, IPCC_CHANNELDirTypeDef ChannelDir);
void (* ChannelCallbackTx[IPCC_CHANNEL_NUMBER])(struct __IPCC_HandleTypeDef *hipcc, uint32_t ChannelIndex, IPCC_CHANNELDirTypeDef ChannelDir);
uint32_t callbackRequest; /*!< Store information about callback notification by channel */
__IO HAL_IPCC_StateTypeDef State; /*!< IPCC State: initialized or not */
} IPCC_HandleTypeDef;
ChannelCallbackRx et ChannelCallbackTx sont des tableaux de pointeurs de fonctions indexés sur l'indice de la ligne d'interruption reçue.
Le fichier src/mbox_ipcc.c fait l'interface entre openAMP et l'IPCC handler. Il vient définir le callback associé à un canal et une direction de notre variable hipcc. Dans l'exemple ci-dessous, on insère à l'indice 0 (valeur de IPCC_CHANNEL_1) du tableau ChannelCallbackRx (via la direction IPCC_CHANNEL_DIR_RX) le pointeur sur fonction IPCC_channel1_callback.
int MAILBOX_Init(void)
{
if (HAL_IPCC_ActivateNotification(&hipcc, IPCC_CHANNEL_1, IPCC_CHANNEL_DIR_RX, IPCC_channel1_callback) != HAL_OK) {
OPENAMP_log_err("%s: ch_1 RX fail\n", __func__);
return -1;
}
...
return 0;
}
Comme le traitement de message par OpenAMP prend du temps, il peut être problématique de lancer la fonction de traitement ou d'envoi de message depuis le contexte de l'interruption (car on bloque le reste du processus et les autres interruptions).
La fonction de callback ne fait que modifier une variable globale du fichier src/mbox_ipcc.c et libérer l'interruption. Par la suite, la fonction main() interroge en boucle la fonction MAILBOX_Poll qui lance le traitement du message ou la réception de l'acquittement si l'une des variable globale a changé.
int MAILBOX_Poll(struct virtio_device *vdev)
{
int ret = -1;
if (msg_received_ch1 == MBOX_BUF_FREE) {
rproc_virtio_notified(vdev, VRING0_ID);
ret = 0;
msg_received_ch1 = MBOX_NO_MSG;
}
return ret;
}
/* Callback from IPCC Interrupt Handler: Master Processor informs that there are some free buffers */
void IPCC_channel1_callback(IPCC_HandleTypeDef * hipcc, uint32_t ChannelIndex, IPCC_CHANNELDirTypeDef ChannelDir)
{
msg_received_ch1 = MBOX_BUF_FREE;
/* Inform A7 that we have received the 'buff free' msg */
OPENAMP_log_dbg("Ack 'buff free' message on ch1\r\n");
HAL_IPCC_NotifyCPU(hipcc, ChannelIndex, IPCC_CHANNEL_DIR_RX);
}
Les VRING sont définis lors de l'initialisation d'OpenAMP dans le fichier src/openamp.c. Le VRING 0 est utilisé pour la transmission tandis que le VRING 1 est utilisé pour la réception.
Enfin, le dernier point important est la notion de endpoint utilisée par OpenAMP et RPMSG. Cette notion permet de définir un destinataire qui est ajouté en en-tête du message envoyé. Ce destinataire est représenté côté micro-contrôleur par une structure fournit par librairie OpenAMP et, côté Linux, par des devices situés dans le répertoire /dev créés le driver RPMSG. C'est le micro-contrôleur qui supervise la création des enpoints.
Prenons notre fichier src/main.c dans lequel nous définissons nos endpoints.
Dans un premier temps, nous venons définir une variable globale qui sera utilisée pour indiqué le type de enpoint a créer côté Linux. Nous reviendrons un peu plus loin sur les autres possibilités.
// Create a ttyRPMSG endpoint on Linux side
#define RPMSG_SERVICE_NAME "rpmsg-tty"
Puis nous créons une variable globale avec la structure rpmsg_endpoint qui sera notre endpoint côté micro-contrôleur.
struct rpmsg_endpoint ept;
La structure rpmsg_endpoint se situe dans STM32CubeMP1/Middlewares/Third_Party/OpenAMP/open-amp/lib/include/openamp/rpmsg.h. Un endpoint est défini, entre-autre, par un nom, une adresse source, une adresse destination et une fonction callback qui sera utilisée par OpenAMP lorsqu'il traitera un message à destination de ce endpoint.
Dans la fonction main(), on attribue les valeurs du enpoint via OpenAMP. Ici, Nous venons créer un endpoint en lui attribuant le callback read_cb qui est une fonction qui copie le message reçu dans un buffer. OPENAMP_create_endpoint n'est qu'un wrapper de la fonction rpmsg_create_ept.
// Setup 'ept' endpoint and tells Linux to create a linked endpoint on its side
status = OPENAMP_create_endpoint(&ept, RPMSG_SERVICE_NAME, RPMSG_ADDR_ANY, read_cb, NULL);
Pour envoyer un message, nous faisons appel à la fonction OPENAMP_send qui va écrire le buffer RxBuffer de taille RxSize en mémoire partagée. OpenAMP fera ensuite appel à la fonction MAILBOX_Notify qui permet de déclencher l'interruption de réception du processeur distant.
// fichier main.c
status = OPENAMP_send(&ept, RxBuff, RxSize);
int MAILBOX_Notify(void *priv, uint32_t id)
{
uint32_t channel;
(void)priv;
/* Called after virtqueue processing: time to inform the remote */
if (id == VRING0_ID) {
channel = IPCC_CHANNEL_1;
OPENAMP_log_dbg("Send msg on ch_1\r\n");
}
...
/* Check that the channel is free (otherwise wait until it is) */
if (HAL_IPCC_GetChannelStatus(&hipcc, channel, IPCC_CHANNEL_DIR_TX) == IPCC_CHANNEL_STATUS_OCCUPIED) {
OPENAMP_log_dbg("Waiting for channel to be freed\r\n");
while (HAL_IPCC_GetChannelStatus(&hipcc, channel, IPCC_CHANNEL_DIR_TX) == IPCC_CHANNEL_STATUS_OCCUPIED)
;
}
HAL_IPCC_NotifyCPU(&hipcc, channel, IPCC_CHANNEL_DIR_TX);
return 0;
}
La fonction MAILBOX_Notify a été assigné au VDEV utilisé par OpenAMP lors de son initialisation. C'est de cette manière qu'il sait quelle fonction appeler lorsqu'il doit notifier le processeur distant qu'un message a été envoyé.
vdev = rproc_virtio_create_vdev(RPMsgRole, VDEV_ID, &rsc_table->vdev, rsc_io, NULL, MAILBOX_Notify, NULL);
Vous pouvez compiler le projet via la commande make. Vous trouverez le binaire dans le dossier build sous le nom rproc-m4-fw.
Pour charger le firmware sur le Cortex-M4, il faut l'envoyer vers le linux et le placer dans le dossier /lib/firmware/
Pour charger le firmware et lancer le Cortex-M4 :
root@stm32mp157f-ev1:~# echo -ne "start" > /sys/class/remoteproc/remoteproc0/state
Normalement vous devriez voir apparaître /dev/ttyRPMSG0 qui montre que nos endpoints ont bien été créés de chaque côté.
Pour pouvoir échanger avec le coprocesseur, il nous suffit d'interagir avec /dev/ttyRPMSG0.
root@stm32mp157f-ev1:~# cat /dev/ttyRPMSG0 & # affichage en continu
root@stm32mp157f-ev1:~# echo "toto" > /dev/ttyRPMSG0 # valeur renvoyée par le Cortex-M4: [CM4] toto
root@stm32mp157f-ev1:~# echo "ping" > /dev/ttyRPMSG0 # valeur renvoyée par le Cortex-M4: [CM4] pong
Pour arrêter le Cortex-M4 :
root@stm32mp157f-ev1:~# echo -ne "stop" > /sys/class/remoteproc/remoteproc0/state
3) Plusieurs endpoints
Il est possible d’avoir plusieurs endpoints pour permettre différents traitements d’informations.
Basculez sur le tag "multiple-endpoints" pour avoir les sources correspondant à cette partie.
git switch --detach multiple-endpoints
Seul le fichier src/main.c a subi des modifications.
Dans cette exemple, nous avons créé un nouvel endpoint nommé ept_echo. Ce dernier retournera le message reçu tel quel, sans passer par la fonction pong qui modifie le message si ce dernier commence par 'ping'.
Nous allons utiliser le même callback étant donné que la première partie du traitement du message est le même. Cependant, nous allons devoir un peu l'ajuster pour pouvoir différencier par la suite si le message était destiné à ept ou ept_echo.
int read_cb(struct rpmsg_endpoint *endpoint, void *data, size_t len, uint32_t src, void *priv)
{
(void) src;
(void) priv;
// copy message
RxSize = len < MAX_COPY_SIZE ? len : MAX_COPY_SIZE - 1;
memcpy(RxBuff + PREFIX_SIZE, data, RxSize);
RxSize += PREFIX_SIZE;
if (endpoint == &ept)
RxMsg = 1;
else
RxMsg = 2;
return 0;
}
Comme précédemment, vous pouvez compiler le code via make et le charger sur la cible. Au démarrage, vous devriez voir cette fois-ci deux endpoints : /dev/RPMSG0 et /dev/RPMSG1 .
Voici un exemple d'utilisation :
root@stm32mp157f-ev1:~# echo -ne "start" > /sys/class/remoteproc/remoteproc0/state
root@stm32mp157f-ev1:~# cat /dev/ttyRPMSG0 &
root@stm32mp157f-ev1:~# cat /dev/ttyRPMSG1 &
root@stm32mp157f-ev1:~# echo ping > /dev/ttyRPMSG0 # valeur renvoyée par le Cortex-M4: [CM4] pong
root@stm32mp157f-ev1:~# echo ping > /dev/ttyRPMSG1 # valeur renvoyée par le Cortex-M4: [CM4] ping
root@stm32mp157f-ev1:~# echo -ne "stop" > /sys/class/remoteproc/remoteproc0/state
4) RPMSG_CHAR & RPMSG_CTRL
Note : Pour cette partie, il vous faudra installer la toolchain arm-none-linux-gnueabihf 13.2.Rel1 ou plus pour pouvoir faire de la compilation croisée vers le processeur A7 : https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads
Pour l'instant, nous utilisons RPMSG via le driver ttyRPMSG, simulant un tty côté Linux, pour interagir avec l'utilisateur. Cependant, il existe une autre méthode passant par les driver RPMSG_ctrl et RPMSG_char.
/!\ Pour pouvoir tester cette partie, vous devez avoir compilé le noyau Linux avec les options CONFIG_RPMSG_CHAR=y et CONFIG_RPMSG_CTRL=y.
Basculez sur le tag "rpmsg-ctrl" pour avoir les sources correspondant à cette partie.
git switch --detach rpmsg-ctrl
Côté Cortex-M4, nous avons juste à changer la valeur du define "RPMSG_SERVICE_NAME" par "rpmsg_ctrl"
#define RPMSG_SERVICE_NAME "rpmsg_ctrl"
Côté Cortex-A7 (Linux), il est nécessaire d'activer les options "RPMSG_CHAR" et "RPMSG_CTRL" dans la configuration du noyau.
Vous pouvez compiler le firmware du micro-contrôleur et le démarrer comme précédemment. Vous constaterez que les /dev/ttyRPMSG* ont disparu et qu'ils ont laissé place à /dev/rpmsg_ctrl0. Contrairement aux périphériques /dev/ttyRPMSG* qui permettaient directement de lire et d'écrire, /dev/rpmsg_ctrl0 permet de créer et manipuler des endpoints.
Avec cette méthode, il nous revient de faire un petit programme de quelques lignes en C faisant appel à des fonctions ioctl pour créer une interface avec l'utilisateur.
Mais ne paniquez pas ! Le code se trouve dans le dossier rpmsg-ctrl/.
Commencer par vous rendre dans le dossier, compiler le code source via make et transférer les 3 binaires générés sur la carte. Nous allons les utiliser au fur et à mesure des explications qui vont suivre.
Le programme issu du fichier rpmsg_create.c permet de créer un endpoint à partir de 4 information : le contrôleur (/dev/rpmsg_ctrl0), un nom, une adresse source et une adresse destination. Le nom peut être choisi à notre convenance. La valeur de l'adresse source n'a en réalité que peu d'importance, c'est RPMSG qui s'en chargera, vous pouvez y mettre un nombre aléatoire. Cependant, l'adresse de destination doit correspondre à l'adresse du endpoint définie par OpenAMP.
Malgré que nous ayons indiqué la valeur RPMSG_ADDR_ANY à OpenAMP lors de la création de notre endpoint, ce dernier ne prend pas une valeur aléatoire. Les adresses définis par OpenAMP commencent à l'adresse 1024 et sont incrémentés de 1 à chaque nouvel endpoint. Donc le endpoint ept est à l'adresse 1024 et ept_echo à l'adresse 1025
Si vous souhaitez définir vos propres adresses, cela est possible en utilisant la fonction rpmsg_create_ept au lieu du wrapper OPENAMP_create_endpoint. Cependant, assurez-vous que l'adresse saisie soit comprise entre 1024 et 1151. Ces valeurs sont définis dans le code source de OpenAMP.
STM32CubeMP1/Middlewares/Third_Party/OpenAMP/open-amp/lib/include/openamp/rpmsg.h:32:#define RPMSG_RESERVED_ADDRESSES (1024)
STM32CubeMP1/Middlewares/Third_Party/OpenAMP/open-amp/lib/include/openamp/rpmsg.h:29:#define RPMSG_ADDR_BMP_SIZE (128)
STM32CubeMP1/Middlewares/Third_Party/OpenAMP/open-amp/lib/rpmsg/rpmsg.c ligne 259 pour le code source de la fonction rpmsg_create_ept().
Le nom, l'adresse source et l'adresse destination sont utilisés pour créer une structure rpmsg_endpoint_info et le périphérique /dev/rpmsg_ctrl0 est utilisé lors de l'ioctl avec la fonction suivante :
#define RPMSG_CREATE_EPT_IOCTL _IOW(0xb5, 0x1, struct rpmsg_endpoint_info)
Utiliser la commande rpmsg_create pour créer le endpoint /dev/rpmsg0 :
root@stm32mp157f-ev1:~# rpmsg-create /dev/rpmsg_ctrl0 test 1234 1024 # Crée un endpoint /dev/rpmsg0
L'étape suivante est d'envoyer un message au co-processeur et de lire la réponse. Le code dédié se trouve dans le fichier rpmsg-ping.c
Il suffit simplement de créer un "file descriptor" via la fonction open et de lire et écrire via read et write.
Le programme rpmsg-ping prend en paramètre un endpoint (ex: /dev/rpmsg0) et un message auquel il ajoute le préfixe "ping: ".
Utiliser la commande rpmsg-ping pour envoyer un message et lire la réponse du co-processeur :
root@stm32mp157f-ev1:~# rpmsg-ping /dev/rpmsg0 toto # pong: toto
Créer un second endpoint (/dev/rpmsg1) ayant pour destination l'adresse 1025 et envoyé le même message à ce nouveau enpoint. Cette fois si le message retourné à l'entête "ping: ". Vous venez de communiquer avec le second enpoint
Pour fermer le endpoint, il nous suffit juste de faire l'appel à l'ioctl suivante avec le file descriptor du endpoint que l'on souhaite supprimer.
#define RPMSG_DESTROY_EPT_IOCTL _IO(0xb5, 0x2)
La fonction rpmsg-destroy prend en paramètre un endpoint (exemple : /dev/rpmsg0)
root@stm32mp157f-ev1:~# rpmsg_destroy /dev/rpmsg0
Les limitations
Dans les précédentes parties, nous avons vu comment STMicroelectronics a conçu sa communication inter-processeurs et comment nous pouvons l’utiliser avec l’interface ttyRPMSG. Cependant nous avons passé sous silence certains points problématiques de la fonctionnalité qui vous ont peut-être sauté aux yeux.
1) Communication Full-duplex
Dans les exemples évoqués ci-dessus, nous avons utilisé deux canaux pour la communication inter-processeur. Voici le schéma de la communication fourni par STMicroelectronics dans le fichier mbox_ipcc.c.
À chaque message reçu sur un canal, un message “buf free” serait renvoyé sur ce même canal comme message d’acquittement, ce qui rendrait l’exemple "full-duplex".
Pourtant, aucun message "buf free" n'est envoyé ni reçu par l'un ou l'autre des processeurs. Ces messages devraient être envoyés par le driver RPMSG, mais ces envois sont volontairement interrompus par STMicroelectronics car il possède déjà un moyen de signaler l’acquittement d’un message via les IPCC.
En d’autres termes, nous avons deux communications "simplex".
Cependant, lorsqu' on tente de configurer l'envoi de message dans les deux sens sur le même canal (ce qui devrait être techniquement possible en reprenant ce que nous avons dit lors de la première partie), la communication ne fonctionne plus.
La source du problème est dans l’implémentation du driver stm32-ipcc et la relation entre les frameworks mailbox et rpmsg.
Le framework MAILBOX sert à fournir une interface entre les drivers de signalisation (IPCC) et ceux de traitement des messages (RPMSG).
La MAILBOX est représentée par des canaux et les lie à une fonction de traitement (dans notre cas, à une fonction de RPMSG).
Le nombre de canaux de la MAILBOX est défini dans le driver de l’IPCC et correspond au nombre de canaux de ce dernier.
L’objectif est de créer une équivalence entre les canaux de l’IPCC et ceux de la MAILBOX.
Rappelons qu’un canal de l’IPCC est bidirectionnel et peut être divisé en deux sous-canaux unidirectionnels et de sens opposés.
De son côté, RPMSG implémente deux virtual queue (VQ). L’une est utilisée pour la réception et la répartition des messages, tandis que l’autre est utilisée pour la transmission. En considérant ces VQ comme des flux de transitions de données, nous pourrions les qualifier d’unidirectionnels.
En assignant une VQ de RPMSG à un canal de la MAILBOX, nous le rendons unidirectionnel.
Hors, nous avons dit plus haut que les canaux de la MAILBOX sont un équivalent des canaux de l’IPCC.
C’est de cette manière que l’utilisation des canaux de l’IPCC est devenue unidirectionnelle et qu’il ne nous est pas possible de mettre en place une communication "full-duplex" sur un seul canal.
Contrairement à son explication, la résolution de ce problème est assez simple. Nous avons juste à modifier le driver stm32-ipcc
diff --git a/drivers/mailbox/stm32-ipcc.c b/drivers/mailbox/stm32-ipcc.c
index b84e05879..c79641773 100644
--- a/drivers/mailbox/stm32-ipcc.c
+++ b/drivers/mailbox/stm32-ipcc.c
@@ -100,7 +100,7 @@ static irqreturn_t stm32_ipcc_rx_irq(int irq, void *data)
dev_dbg(dev, "%s: chan:%d rx\n", __func__, chan);
- mbox_chan_received_data(&ipcc->controller.chans[chan], NULL);
+ mbox_chan_received_data(&ipcc->controller.chans[chan * 2], NULL);
stm32_ipcc_set_bits(&ipcc->lock, ipcc->reg_proc + IPCC_XSCR,
RX_BIT_CHAN(chan));
@@ -134,7 +134,7 @@ static irqreturn_t stm32_ipcc_tx_irq(int irq, void *data)
stm32_ipcc_set_bits(&ipcc->lock, ipcc->reg_proc + IPCC_XMR,
TX_BIT_CHAN(chan));
- mbox_chan_txdone(&ipcc->controller.chans[chan], 0);
+ mbox_chan_txdone(&ipcc->controller.chans[chan * 2 + 1], 0);
ret = IRQ_HANDLED;
}
@@ -144,7 +144,7 @@ static irqreturn_t stm32_ipcc_tx_irq(int irq, void *data)
static int stm32_ipcc_send_data(struct mbox_chan *link, void *data)
{
- unsigned long chan = (unsigned long)link->con_priv;
+ unsigned long chan = (unsigned long)link->con_priv / 2;
struct stm32_ipcc *ipcc = container_of(link->mbox, struct stm32_ipcc,
controller);
@@ -292,7 +292,7 @@ static int stm32_ipcc_probe(struct platform_device *pdev)
ipcc->controller.dev = dev;
ipcc->controller.txdone_irq = true;
ipcc->controller.ops = &stm32_ipcc_ops;
- ipcc->controller.num_chans = ipcc->n_chans;
+ ipcc->controller.num_chans = ipcc->n_chans * 2;
ipcc->controller.chans = devm_kcalloc(dev, ipcc->controller.num_chans,
sizeof(*ipcc->controller.chans),
GFP_KERNEL);
L’objectif ici est de ne plus lier les canaux de la MAILBOX avec ceux de l’IPCC, mais plutôt de les lier avec les sous-canaux. Comme chaque canal de l’IPCC contient deux sous-canaux, nous multiplions par deux le nombre de canaux de la MAILBOX. Nous faisons également en sorte que lors de l’utilisation de ces derniers, ceux dédiés à la réception soient d’indice pair et ceux employés à la transmission soient d’indice impair, toujours dans cet objectif de représenter les sous-canaux de l’IPCC à travers les canaux de la MAILBOX.
Cette petite modification va nécessiter de mettre à jour le device-tree de la carte tel que ci-dessous :
--- a/arch/arm/boot/dts/stm32mp157f-ed1.dts
+++ b/arch/arm/boot/dts/stm32mp157f-ed1.dts
@@ -359,7 +359,7 @@ &iwdg2 {
&m4_rproc {
memory-region = <&retram>, <&mcuram>, <&mcuram2>, <&vdev0vring0>,
<&vdev0vring1>, <&vdev0buffer>, <&mcu_rsc_table>;
- mboxes = <&ipcc 0>, <&ipcc 1>, <&ipcc 2>, <&ipcc 3>;
+ mboxes = <&ipcc 0>, <&ipcc 3>, <&ipcc 5>, <&ipcc 7>;
mbox-names = "vq0", "vq1", "shutdown", "detach";
interrupt-parent = <&exti>;
interrupts = <68 1>;
Attention ! Il ne faut pas confondre le nom des canaux de l'IPCC allant de 1 à 6 avec l'indice de ces derniers allant de 0 à 5. Le canal 1 de l'IPCC a pour indice 0.
Les indices des canaux de la MAILBOX dédiés deviennent :
- Dans le sens RXO (réception) : indice_canal_IPCC * 2
- Dans le sens TXF (transmission) : indice_canal_IPCC * 2 + 1
Maintenant, il nous est possible de définir l’envoi et la réception sur un seul canal en changeant les valeurs correspondantes dans le device-tree.
Dans notre configuration originale, seule la MAILBOX d’indice 0 était dédiée à la réception.
Dans la partie précédente, nous avions (côté Linux) la réception sur le canal 1 de l'IPCC (indice 0 de la MAILBOX) et l’envoie sur le canal 2 de l'IPCC (indice 3 de la MAILBOX), correspondant aux noms vq0 et vq1. En mettant à 1 l’indice correspondant à vq1, l’envoi de message passera par le canal 1 (<indice_canal_IPCC> * 2 + 1 = <indice_mailbox_TXF>).
Pour faire en sorte que l’envoi de message passe par le canal 1 de l'IPCC (d'indice 0), il faut passer l'indice correspondant à vq1 à 1 (<indice_canal_IPCC> * 2 + 1 = <indice_mailbox_TXF>). Pour faire de la réception sur le canal 4 de l'IPCC (d'indice 3), il faut passer l'indice correspondant à vq0 à 6, etc...
Il ne restera plus qu’à modifier la valeur des canaux employés dans le fichier mbox-ipcc.c de l’application fonctionnant sur le cortex-M4.
Ainsi nous sommes dorénavant capable de mettre en place une communication Full-duplex sur un seul canal comme l'indique la documentation !
2) Nombre de flux d’échange de données limité à 2
Nous avons vu comment corriger l’implémentation de l’IPCC pour pouvoir définir une communication comme indiqué dans la documentation de ST. En rectifiant cela, nous avons rendu aux 6 canaux de l’IPCC leur fonctionnalité bidirectionnelle.
Nous serions donc techniquement capables de définir 12 flux à sens unique, chacun utilisant un sous-canal de l’IPCC.
Dans la pratique, nous sommes limités à deux flux unidirectionnels. La raison tient dans une ligne du fichier /include/linux/remoteproc.h du noyau Linux.
Le nombre de VRINGS, correspondant au nombre de VQ, est limité à 2 par la définition de RVDEV_NUM_VRINGS. Or comme on l'a dit précédemment, RPMSG utilise deux VQ pour traiter la réception et l'envoi de message et ce sont ces dernières que nous venons attacher aux canaux de MAILBOX. Il ne nous est donc pas possible d’ajouter un échange de données supplémentaire.
Pourtant, nous pouvons voir dans le patch du device-tree de la partie précédente que deux canaux supplémentaires sont utilisés : “shutdown” et “detach”. Ces canaux sont utilisés comme des signaux pour indiquer au processeur distant une action à faire.
Par exemple, l’interruption IPCC peut être utilisée pour avertir le coprocesseur qu’il va être arrêté. Ce dernier aura donc la possibilité de sauvegarder certaines données puis d’utiliser l’interruption d’acquittement du même canal pour indiquer au processeur principal qu’il est prêt à être éteint.
En conclusion
Le support de STMicroelectronics pour la communication inter-processeurs est fonctionnel et la mise en place est assez simple, malgré le fait que, dans l'état, nous n'avons accès qu'à la moitié des capacités de communication.
Nous sommes mi-2025 et STMicroelectronics a sorti une nouvelle gamme de processeurs applicatifs : les STM32MP2. Nous pouvons espérer que le support évolue et que certains problèmes soient réglés à l'avenir.