Pourquoi un protocole hôte ?

Dans l'état d'avancement d'Opener, nous expliquions que la stack se construit de bas en haut et que seul le driver est terminé aujourd'hui. Or nos cartes ont besoin de plus qu'un driver. La NR+Gate associe un nRF9151, qui porte le modem NR+, à un module Linux Rockchip RK3566. Pour faire un sniffer, une interface web ou un outil de test sur cette passerelle, le logiciel Linux doit pouvoir commander la radio.

C'est exactement le modèle du RCP (Radio Co-Processor) d'OpenThread : le microcontrôleur radio n'exécute que la partie temps réel, et un hôte le pilote à travers une liaison série avec un protocole appelé Spinel. Nous avons choisi de réutiliser Spinel plutôt que d'inventer un protocole maison, pour trois raisons :

  • Un format éprouvé : encodage compact des types, identifiants de transaction, propriétés génériques get/set/insert, plages réservées aux extensions des fabricants.
  • Des transports existants : le tramage HDLC-lite pour l'UART et le tramage SPI d'OpenThread sont spécifiés et largement déployés.
  • Des outils réutilisables : le codec C d'OpenThread s'intègre tel quel, et un outil comme pyspinel peut dialoguer avec un appareil Opener.

Un contrôle par niveaux

Cette première version expose le niveau driver. L'hôte ordonne les opérations radio (TX, RX, RSSI1), lit le temps du modem et les capacités radio, et reçoit les événements du driver. Les couches MAC, DLC et de convergence tournent alors sur l'hôte. Des plages de propriétés sont réservées pour les niveaux supérieurs : quand la stack embarquée aura une MAC, une version suivante pourra l'exposer sans casser le protocole.

PlageNiveauÉtat
0x3C00–0x3C7FLaissée libre (utilisée par ESP-IDF)—
0x3C80–0x3CFFCœur OpenerRéservée
0x3D00–0x3DFFDriverImplémentée
0x3E00–0x3EFFMACRéservée
0x3F00–0x3F7FDLCRéservée
0x3F80–0x3FFFConvergence et applicationsRéservée

L'appareil annonce les niveaux qu'il expose dans PROP_CAPS, et s'identifie avec un nouveau type d'interface, 0x4E52 (« NR »). Un hôte qui ne parle que les propriétés Thread ou IEEE 802.15.4 reçoit simplement PROP_NOT_FOUND.

Du driver Opener aux propriétés Spinel

Le travail de conception a consisté à faire correspondre chaque fonction de l'API driver Opener (dectnrp_driver.h) à une commande Spinel sur une propriété. Le résultat tient en une dizaine de propriétés :

API driverSpinelRemarque
start() / stop()SET DRV_ENABLED (0x3D00)Le driver reste arrêté au démarrage, jusqu'à ce que l'hôte l'active.
get_capabilities()GET DRV_CAPS (0x3D01)Capacités radio ETSI TS 103 636-4, ticks par symbole, taille de trame, nombre de slots et latences du driver.
get_time()GET DRV_TIME (0x3D02)Temps modem, en ticks (64 bits).
configure()SET DRV_NETWORK_ID, DRV_FILTER_SHORT_NWID, DRV_FILTER_SHORT_RXADDRIdentifiant réseau et filtres de réception.
schedule(op)INSERT DRV_OP_QUEUE (0x3D10)Une opération TX, RX ou RSSI1, immédiate ou planifiée.
Callback d'événementsDRV_EVENT (0x3D11), non sollicitéTrame reçue, erreur, résultats RSSI1, fin d'opération.
—GET DRV_STATS (0x3D20)Sept compteurs d'interface, pour le diagnostic.

Les opérations : un jeton choisi par l'hôte

L'essentiel passe par DRV_OP_QUEUE. Chaque insertion porte un préfixe commun (jeton, type, canal, drapeaux, date de début en ticks modem), suivi d'un suffixe propre au type d'opération : paramètres de modulation et données pour TX, durée et RSSI attendu pour RX, nombre de sous-slots pour RSSI1. Les drapeaux couvrent la planification, le LBT, le HARQ, les beacons et le mode de réception. L'appareil rejette avec INVALID_ARGUMENT tout drapeau qui n'a pas de sens pour le type d'opération.

Le jeton est choisi par l'hôte et doit être unique parmi les opérations en cours. L'appareil répond INSERTED une fois que le driver a accepté l'opération, puis chaque événement qui en découle porte ce jeton. Les erreurs du driver sont traduites en statuts Spinel : -ENOMEM devient NOMEM, -EBUSY devient BUSY, et ainsi de suite. Les autres valeurs d'errno sont transmises dans une plage de statuts fabricant.

Des garanties de livraison explicites

Un hôte qui pilote une radio à distance doit savoir quand il peut réutiliser une ressource. Nous avons donc écrit le contrat noir sur blanc :

  • après INSERTED, l'appareil envoie exactement un événement OP_FINISHED pour ce jeton, et c'est le dernier événement de l'opération ;
  • les autres événements partagent un pool de buffers. S'il est plein, ils sont abandonnés, comptés, et le nombre d'abandons est signalé dans l'événement suivant ;
  • OP_FINISHED dispose de son propre buffer par slot d'opération : il n'est jamais abandonné, et l'hôte n'attend jamais indéfiniment ;
  • CMD_RESET arrête le driver, attend les fins d'opération, libère tous les slots et invalide tous les jetons, sans redémarrer l'appareil.
Exemple : lire la version du protocole (UART)
# Hôte → appareil : TID 1, CMD_PROP_VALUE_GET, PROP_PROTOCOL_VERSION
7E 81 02 01 <FCS> 7E

# Appareil → hôte : TID 1, CMD_PROP_VALUE_IS, PROP_PROTOCOL_VERSION = 4.3
7E 81 06 01 04 03 <FCS> 7E

L'implémentation dans Opener

L'interface Spinel est un sous-système optionnel d'Opener (subsys/spinel). Elle se place entre le driver et le transport, et se découpe en blocs aux responsabilités nettes :

Hôte
Linux ou PC : la logique protocolaire (dans notre cas, notre implémentation en Rust).
Transport
uart_hdlc.c, spi.c, none.c : le tramage sur la liaison. Le devicetree choisit le transport, et le transport none sert aux tests.
Cœur
spinel_core.c : threads RX et TX, dispatch des commandes, propriétés d'identité et de configuration du driver, reset.
Broker
spinel_ops.c : slots d'opérations, appels à schedule(), conversion des événements du driver en trames DRV_EVENT.
Codec
lib/spinel.c : le codec d'OpenThread, intégré sans aucune modification (BSD-3-Clause) et figé à un commit précis.
L2
l2.c : au niveau driver, la L2 vit sur l'hôte. Spinel fournit donc le point d'accroche L2 que le driver appelle à l'initialisation, et l'interface réseau reste inactive côté appareil.
Driver
Driver Opener (nRF91x1, ou le driver de simulation en test), utilisé à travers son API publique uniquement.

Toute la mémoire est bornée et dimensionnée par Kconfig : 8 slots d'opérations, 16 buffers d'événements, 4 buffers de réception et 4 d'émission par défaut, avec des trames d'au plus 1300 octets (la MTU NR+ plus les en-têtes). Il n'y a aucune allocation dynamique. L'activation tient en quelques lignes :

prj.conf
CONFIG_NETWORKING=y
CONFIG_DECTNRP_DRIVER_SCHEDULED_API=y
CONFIG_DECTNRP_SPINEL=y

UART : HDLC-lite

Sur UART, les trames suivent le tramage HDLC-lite de Spinel : délimiteur 0x7E, échappement des octets de contrôle et FCS CRC-16/X-25. Les deux sens fonctionnent sur interruption, à 1 Mbaud. Le contrôle de flux matériel est indispensable : sans lui, l'UARTE perd des octets de l'hôte dès que son interruption est servie en retard. C'est le transport que nous utilisons avec les kits de développement nRF9151 DK et nRF9131 EK, branchés en USB sur un PC.

SPI : le tramage OpenThread, plus une ligne BUSY

Sur la NR+Gate, le nRF9151 est esclave SPI du RK3566. Nous reprenons le tramage SPI d'OpenThread : chaque transaction commence, dans les deux sens, par un en-tête de 5 octets (drapeaux, taille maximale acceptée, taille de la trame envoyée), suivi de la trame Spinel brute, sans HDLC.

Devicetree : NR+Gate
&spi2 {
        compatible = "nordic,nrf-spis";
        def-char = <0xff>;

        spinel@0 {
                compatible = "opener,spinel-spi";
                reg = <0>;
                spi-max-frequency = <8000000>;
                irq-gpios = <&gpio0 9 GPIO_ACTIVE_HIGH>;
                busy-gpios = <&gpio0 26 GPIO_ACTIVE_HIGH>;
        };
};

Le SPI pose un problème que l'UART n'a pas : l'esclave ne peut rien envoyer tant que le maître ne cadence pas, et une transaction lancée alors que l'esclave n'est pas prêt est perdue. Deux lignes complètent donc le bus :

  • IRQ : l'appareil a une trame pour l'hôte ;
  • BUSY, notre extension de la spécification OpenThread : l'appareil n'est pas prêt pour une transaction. Il passe BUSY à la fin de chaque transaction et le relâche une fois la suivante armée, après avoir mis IRQ à jour. L'hôte lit donc IRQ uniquement quand BUSY est inactif.

Pour réduire la latence, l'appareil retient BUSY après une transaction qui lui a apporté une requête, au plus 2 ms par défaut, le temps que la réponse soit prête. La réponse part alors dans la transaction suivante de l'hôte, qui peut elle-même porter la requête suivante : un aller-retour tient en deux transactions. Et si aucun hôte ne lit, les trames en attente derrière la trame proposée sont abandonnées au bout d'une seconde, pour ne pas épuiser les buffers.

Côté matériel : le caractère par défaut du contrôleur SPI esclave (def-char) est fixé à 0xFF. Une transaction cadencée quand l'appareil n'est pas armé lit donc un en-tête invalide, que l'hôte ignore sans risque de confusion.

Le sample dectnrp-spinel d'opener-samples fournit un firmware prêt à l'emploi : UART pour le nRF9151 DK et le nRF9131 EK, SPI pour la NR+Gate. Son main() se contente d'afficher une bannière : l'interface démarre seule au boot.

Tester avec Twister

Un protocole de contrôle radio se teste mal à la main : les bugs intéressants sont dans les cas limites (pool plein, reset pendant une opération, transaction SPI au mauvais moment). Nous avons donc écrit les tests en même temps que le code, avec ztest et Twister, l'outil de test de Zephyr. Tout tourne sur native_sim, sans matériel.

ScénarioCe qu'il vérifie
opener.spinel.codecIntégration du codec OpenThread.
opener.spinel.hdlcTramage HDLC-lite : échappement, FCS, trames invalides ou trop longues.
opener.spinel.spi_linkEn-tête SPI : drapeaux, motif, longueurs, trames tronquées.
opener.spinel.coreDispatch, identité, propriétés du driver, statuts d'erreur, reset.
opener.spinel.opsBroker d'opérations : jetons en double, slots épuisés, pool d'événements plein, reset avec des opérations en cours.
opener.spinel.mock / mock_autoLe driver de simulation lui-même, y compris son mode qui émet des trames périodiques.
opener.spinel.spi_transportLe transport SPI complet, face à un faux contrôleur SPI esclave : ordre des trames, maintien de BUSY, transactions ignorées, abandons.
opener.spinel.simdevUn appareil simulé complet (compilation uniquement), utilisé par les tests de l'hôte.

Deux briques rendent ces tests possibles. Un driver de simulation implémente l'API driver Opener avec une horloge modem simulée à 69,12 MHz : les tests choisissent les événements qu'il renvoie, ou le laissent générer du trafic tout seul. Un faux contrôleur SPI esclave, déclaré dans le devicetree de test, permet de jouer le rôle du maître transaction par transaction et de vérifier les lignes IRQ et BUSY à chaque étape.

Lancer les tests
west twister -T deps/dectnrp-opener/tests/subsys/spinel -p native_sim/native/64
9
scénarios Twister
76/76
cas de test réussis
~75 s
sur un PC, sans matériel

Le scénario simdev mérite une mention. Il compile un firmware Spinel complet pour native_sim, avec le driver de simulation, et expose l'UART Spinel sur un pseudo-terminal du PC. Pour l'hôte, c'est un vrai appareil : nos tests d'intégration côté hôte s'y connectent comme à une carte réelle. Le protocole est ainsi testé de bout en bout, firmware et hôte réunis, sans aucun matériel.

Puis sur du vrai matériel

Les tests sur native_sim valident la logique, mais pas le comportement électrique d'un vrai bus. L'interface a donc aussi été testée sur notre NR+Gate : le firmware dectnrp-spinel sur le nRF9151, piloté en SPI par le RK3566, avec de vraies trames NR+ sur l'air. C'est dans cette configuration qu'elle a fait tourner nos démos au pre-event DECT World 2026, en sniffer comme en pilotage à distance.

Et ensuite ?

L'interface hôte Spinel figure dans la partie « au-delà » de la feuille de route d'Opener. Notre implémentation vit aujourd'hui sur la branche spinel-ctrl de notre fork public d'Opener, et nous comptons la proposer en amont. Quelques points restent à discuter avec la communauté :

  • la valeur du type d'interface (0x4E52) et de l'identifiant fabricant, à faire accepter en amont de Spinel ;
  • le CRC optionnel du tramage SPI, que nous ne gérons pas encore ;
  • l'exposition des niveaux MAC, DLC et CVG, une fois ces couches disponibles dans la stack embarquée.
Position Codium — Nov. 2026

Reprendre Spinel nous a évité d'inventer un protocole, et transformé la radio Opener en co-processeur pilotable depuis n'importe quel hôte. Le vrai travail a été ailleurs : écrire un contrat de livraison clair, puis le tester jusque dans les cas limites, d'abord sans matériel, puis sur nos passerelles.

Une passerelle NR+ à concevoir ?
Firmware embarqué, BSP Linux, liaison hôte et radio : nous concevons des produits DECT NR+ complets. Parlons de votre architecture.
Discuter de votre projet Nos cartes DECT NR+