Aller au contenu

3 façons d’écrire sur le bus SPI avec Arduino (via librairie SPI.h, en faisant du « hardware SPI », ou du « bit banging »)

Tuto écrire sur bus SPI avec Arduino, pour envoyer des données depuis le microcontrôleur ATmega328P à un périphérique via liaison série

La plupart du temps, on trouve des librairies déjà toutes faites, « prêtes à l’emploi », pour faire communiquer nos périphériques SPI (sondes, capteurs, convertisseurs, émetteurs/récepteurs, …) avec notre Arduino. Cela étant dit, il arrive parfois que ces librairies ne soient pas adaptées à nos besoins, ou tout simplement, inexistantes (si vous utilisez un produit très récent, ou très peu utilisé).

Pour pallier à cela, je vous propose de découvrir ici 3 façons de communiquer en SPI (sans librairie spécifique), sans avoir à importer/installer quoi que ce soit, au niveau de l’IDE Arduino ! Le sujet étant vaste, je vous propose de voir aujourd’hui « comment écrire sur le bus SPI avec Arduino » (avec exemple pratique à l’appui), et dans un prochain article, « comment lire sur le bus SPI ». Ainsi, vous aurez une bonne vue d’ensemble ! Ça vous dit ? Alors c’est parti 😉

Ce contenu vous plaît ? Alors abonnez-vous à la Newsletter pour ne rien louper !

Remarque : prendre en main la communication SPI, sans librairie spécifique, nécessite de connaître le datasheet du périphérique SPI avec lequel vous souhaitez communiquer (et celui du µC monté sur votre carte Arduino, par la même occasion). Aussi, si vous débutez en électronique, sachez que ce qui suit pourrait facilement vous rebuter et vous décourager, tant il y a d’informations techniques à découvrir, comprendre, et assimiler. Mais cela est le prix à payer pour avancer, et se perfectionner !

Intro : dans quel cas avons-nous besoin d’« écrire » sur le bus SPI ?

Tout d’abord, qu’est-ce j’entends par « écrire sur le bus SPI » ? En fait, c’est un abus de langage (une façon de parler, si vous préférez) pour décrire l’action d’envoyer des données, d’un « maître » à un « esclave » (sans forcément attendre de réponse en retour, en fait).

On « écrit » donc sur le bus SPI à chaque fois qu’on souhaite envoyer une commande / consigne / ordre, accompagné d’éventuelles données, à un destinataire particulier (ce qui est le propre d’une communication SPI par essence, puisqu’elle est du type « maître-esclave(s) »).

Au passage, si vous souhaitez approfondir vos connaissances sur la communication SPI, n’hésitez pas à jeter un coup d’œil sur l’article que j’avais fait sur les liaisons séries, où je détaille notamment la liaison série SPI.

Remarque : une des méthodes que je vous présente ici, nommée le « bit banging », vous permettra même d’émuler une communication SPI. Ainsi, même si votre microcontrôleur est dépourvu de bus/registres SPI, vous pourrez quand même communiquer avec un périphérique SPI !

Comment écrire sur le bus SPI avec Arduino ?

En pratique, il y a globalement 4 principales façons de faire, pour écrire sur le bus SPI avec Arduino (envoyer des ordres/données, d’un maître à un esclave, pour rappel) :

  • en utilisant une librairie spécifique, qui est « 100% » dédiée au périphérique avec lequel on souhaite communiquer
  • en utilisant une libraire généraliste, telle que la librairie SPI.h (native sous Arduino IDE, donc rien besoin d’installer)
  • en manipulant les registres du microcontrôleur présent sur sa carte Arduino (c’est ce que j’appellerai le « hardware SPI », ici)
  • en faisant du bit banging, c’est à dire en envoyant 1 à 1 tous les bits constituant notre message à transmettre sur le bus SPI

Présenté de la sorte, je vous dirais que ça va de la façon la « plus simple » à mettre en œuvre, à la « plus compliquée » ! En effet, il est clair que le plus simple est d’utiliser une librairie déjà toute faite, prête à l’emploi donc, où l’on a pas à ce soucier de quoi que ce soit (au niveau de la communication SPI, j’entends).

Mais comme évoqué en intro, le but du présent article est plutôt de vous montrer comment communiquer en SPI, lorsqu’aucune libraire spécifique ne répond à nos besoins, ou n’existe encore. C’est pourquoi nous allons voir ici les 3 autres façons de faire, de la liste ci-dessus (librairie généraliste, manipulation de registres, puis bit banging).

Ainsi, vous ne serez plus dépendant de la moindre librairie spécifique, pour communiquer avec vos périphériques SPI !

Par contre, cela nécessitera de connaître le datasheet du microcontrôleur embarqué sur votre carte Arduino (l’ATmega328P par exemple, si vous utilisez une carte Arduino Uno ou Nano), et le datasheet du périphérique SPI avec lequel vous prévoyez de communiquer.

Tout cela vous paraît abstrait ? Alors sans plus attendre, passons à un montage électronique concret, afin de pouvoir apprendre progressivement, avec un support visuel à l’appui 😉

Montage exemple : Arduino → DAC (convertisseur numérique vers analogique) en SPI

Alors, pour rendre cet article concret, je vous propose de partir sur le montage suivant : un DAC (convertisseur numérique vers analogique), branché sur le bus SPI d’un Arduino, lui-même piloté par un potentiomètre.

Avec cela, nous allons réaliser un générateur de tension variable, piloté par potentiomètre. Dans cette optique, l’arduino :

  • lira la tension analogique présente sur le potentiomètre
  • convertira cette tension au format numérique
  • et transmettra cette valeur numérique au DAC, via le bus SPI, pour qu’il en ressorte une tension analogique

En bref, il y a conversion analogique > numérique > analogique, avec communication SPI entre Arduino et DAC.

Voici ce que cela donne en image, au niveau fonctionnel :

Schéma fonctionnel montage test écriture spi pour envoyer des données et consignes, avec potentiomètre nano et DAC SPI type MCP4921

Pour info, le DAC utilisé ici sera le MCP 4921 de Microchip ; c’est en fait un convertisseur numérique à analogique de 12 bits (4096 niveaux de tension, donc), pilotable via le bus SPI.

Au niveau du schéma électrique filaire, voici ce que donne le montage de test d’écriture SPI :

Schéma montage de test pour écriture bus SPI, utilisant convertisseur numérique analogique MCP 4921 de 12 bits, communication série via Arduino

Nota : la sortie VOUTA délivrera une tension égale à x / 4096 * VREFA (avec x la valeur de 0 à 4095 qu’on envoie, et VREFA= VDD = +5V, sur notre montage ici).

Liste des composants utilisés :

Et une fois câblé sur breadboard, voici à quoi le montage ressemble :

Maintenant, passons à la partie logicielle, pour voir comment piloter cela (et ce, de 3 manières différentes, comme promis !).

Remarque : ce montage sera le même, tout au long de l’article. Seul le code de programmation Arduino changera !

Méthode #1 : utilisation de la librairie SPI.h native sous Arduino IDE

Pour commencer, nous allons piloter le DAC via la librairie SPI.h, native sous l’IDE Arduino (aucun besoin d’installer quoi que ce soit, par conséquent).

Par contre, vous aurez besoin de lire et comprendre le datasheet du MCP4921 (convertisseur numérique → analogique, aussi appelé « DAC » en anglais), notamment page 24 et 25 du PDF, où sont détaillés la « commande d’écriture », et le protocole SPI détaillé. Du reste, vous trouverez un maximum de commentaires dans le code, pour faciliter la compréhension !

Ah oui… pour faire simple, le DAC modèle MCP 4921 a simplement besoin qu’on lui envoie une séquence de 16 bits via le bus SPI, constitués de :

  • 4 bits de configuration (toujours laissés à « 0011 », dans notre cas, comme détaillé dans le code)
  • et 12 bits de données

Remarque : comme l’Arduino Nano est doté d’un microcontrôleur à registres 8 bits pour la partie SPI (modèle ATmega328P), nous enverrons ces 16 bits en 2 x 8 bits consécutifs, tout simplement !

À noter que puisqu’il s’agit là d’un DAC de 12 bits (donc 212 niveaux de tension, soit 4096 niveaux possibles), les 12 bits de données correspondront tout simplement à la représentation de tensions souhaitée en sortie de DAC. Ainsi, dans le cas de notre montage test :

  • « 0000 0000 0000 » correspondra à 0 / 4096 * VREFA, soit 0V
  • « 1111 1111 1111 » correspondra à 4095 / 4096 * VREFA, soit quasi 5V
  • et toutes les valeurs intermédiaires « x » correspondront à x / 4096 * VREFA, tout simplement

À présent, passons à la partie codage de tout cela, avec la librairie SPI.h :

/*
   ______               _                  _///_ _           _                   _
  /   _  \             (_)                |  ___| |         | |                 (_)
  |  [_|  |__  ___  ___ _  ___  _ __      | |__ | | ___  ___| |_ _ __ ___  _ __  _  ___  _   _  ___
  |   ___/ _ \| __|| __| |/ _ \| '_ \_____|  __|| |/ _ \/  _|  _| '__/   \| '_ \| |/   \| | | |/ _ \
  |  |  | ( ) |__ ||__ | | ( ) | | | |____| |__ | |  __/| (_| |_| | | (_) | | | | | (_) | |_| |  __/
  \__|   \__,_|___||___|_|\___/|_| [_|    \____/|_|\___|\____\__\_|  \___/|_| |_|_|\__  |\__,_|\___|
                                                                                      | |
                                                                                      \_|
  Fichier :       prgArduino-TestDAC-1-LibrairieSPIh.ino

  Description :   Programme permettant de piloter un DAC, via SPI

  Remarques :     --> Méthode utilisée : utilisation de la librairie SPI.h

  Auteur :        Jérôme TOMSKI (https://passionelectronique.fr/)
  Créé le :       24.04.2023

*/

#include <SPI.h>

#define brocheSSduDAC     3     // Pour activer la communication SPI avec le DAC (actif au niveau bas)
#define pin_POTENTIOMETRE A0    // La valeur lue sur A0 (potentiomètre), sera envoyée au DAC, pour "reproduction" de la tension (entre 0 et 5V)

// Ligne slave-select du DAC (branchée sur sortie D3 de la carte Arduino, soit la pin PD3 du µC, donc le bit 3 sur PORTD[7..0])
#define selectionner_DAC        PORTD &= 0b11110111           // Pour rappel : "&=" effectue un "ET logique", et "|=" un "OU logique"
#define desactiver_DAC          PORTD |= 0b00001000

void setup() {
  pinMode(brocheSSduDAC, OUTPUT);         // Défini la broche arduino comme sortie, pilotant la broche SS du DAC
  digitalWrite(brocheSSduDAC, HIGH);      // ... et la fixe à l'état haut (communication SPI/DAC désactivée)
  
  SPI.begin();                            // Initialise la librairie SPI
  SPI.setClockDivider(SPI_CLOCK_DIV2);    // On divise l'horloge 16 MHz du µC par 2, pour fonctionner à 8 MHz (nota : ce DAC peut fonctionner jusqu'à 20 MHz, si on souhaite)
  
  Serial.begin(9600);                     // Initialise la communication série, avec le PC
  Serial.println(F("================================================================================"));
  Serial.println(F("Test du DAC modèle MCP 4921, de chez Microchip"));
  Serial.println(F("- Méthode de communication : librairie SPI.h"));
  Serial.println(F("================================================================================"));
  Serial.println("");
}

void loop() {

  uint16_t valeurPOT = 0;

  // Lecture valeur d'entrée arduino, sur 10 bits (0..1023)
  valeurPOT = analogRead(pin_POTENTIOMETRE);

  // Conversion en valeur 12 bits (0..4095)
  valeurPOT = map(valeurPOT, 0, 1023, 0, 4095);

  // Affichage de cette valeur sur le moniteur série
  Serial.print(F("Valeur de l'entrée A0 (potentiomètre) = "));
  Serial.println(valeurPOT);

  // Activation de la communication avec le DAC (en abaissant sa ligne /SS)
  selectionner_DAC;

  // Ajout de 4 bits pour la configuration du DAC, "à gauche" des 12 bits de données
      // 1er bit de config : toujours à 0, obligatoirement (cf. datasheet)
      // 2ème bit de config : 0=unbuffered, 1=buffered (ici choix "unbuffured", donc 0)
      // 3ème bit de config : 0=gain double, 1=pas de gain (ici choix "pas de gain", donc 1)
      // 4ème bit de config : 0=sortie DAC éteinte, 1=sortie DAC active (ici choix "sortie du DAC active", donc 1)
      // ... d'où les quatre bits "0011" qui seront ajoutés "devant" les 12 bits de données
  valeurPOT |= 0b0011000000000000;      // On rajoute donc "0011" devant la valeur du potentiomètre (le "|=" étant un "OU logique", pour rappel)

  // Transmission des données
  SPI.transfer((uint8_t)(valeurPOT >> 8));                    // Transfert des 8 bits de "gauche" (pour envoyer les 8 bits de poids fort, en premier)
  SPI.transfer((uint8_t)(valeurPOT & 0b0000000011111111));    // Puis transfert des 8 bits de "droite" (pour envoyer les 8 bits de poids faible, en second)

  // Désactivation de la communication avec le DAC (en remontant sa ligne /SS)
  desactiver_DAC;
 
  delay(1000);   // Petit délai avec rebouclage
}

Au passage, dans ce code, j’ai utilisé des « fonctions raccourcies ». Vous avez certainement dû les remarquer, en début de programme, avec le « #define selectionner_DAC PORTD &= 0b11110111 », et le « #define desactiver_DAC PORTD |= 0b00001000 ». Ces fonctions là permettent tout simplement de manipuler les ports d’entrées/sorties de l’Arduino, et ce, très rapidement. En fait, elles sont équivalentes à la fonction « digitalWrite », mais en plus rapide 😉

Pourquoi avoir fait ce choix, me direz-vous ? Par habitude, et pour vous montrer une autre façon de coder (même si cela n’était pas nécessaire ici, car nous avons des pauses de 1 seconde entre chaque rebouclage de la fonction loop). Du reste, sachez que ce type d’écriture abrégé est parfois indispensable, lorsqu’on souhaite aller vite, et donc, ne pas perdre de temps. C’est pourquoi je vous montre cette autre manière de faire, en manipulant directement les registres d’E/S du microcontrôleur, afin de piloter nous même l’état des sorties qui nous intéressent (ici, la ligne de sélection SPI, notée « brocheSSduDAC » dans le code, qui est branchée sur la broche D3 de l’Arduino).

Cela étant dit, le reste programme est somme toute assez simple :

  • on lit la tension aux bornes du potentiomètre
  • on convertit cette valeur 10 bits (fournie par la fonction « analogRead ») en valeur 12 bits (« exigée » par notre DAC), grâce à la fonction « map »
  • on sélectionne notre DAC, en abaissant sa ligne « slave select » (SS, ou aussi nommée CS, pour « chip select »)
  • on rajoute les 4 bits de configuration devant les 12 bits de données (ce qui nous fait 16 bits, au total)
  • on envoie ces 16 bits (en 2 fois 8 bits consécutifs, car c’est une limite physique imposée par la taille des registres du µC de la carte Arduino)
  • on attend 1 seconde, et on reboucle à l’infini !

Nota :
– si vous ouvrez le moniteur série de votre IDE Arduino, vous verrez s’afficher la valeur du potentiomètre, qui sera envoyée au DAC
– et si vous branchez un voltmètre sur la sortie VOUTA, vous pourrez visualiser la variation de tension entre 0 et 5V, lorsqu’on varie le potentiomètre, d’un bout à l’autre

Test DAC 12bits sur Arduino avec vue sur moniteur série, utilisation de la librairie SPI.h pour la communication série avec convertisseur électronique

J’espère que tout cela ne vous paraît pas trop compliqué ! Surtout que la suite va … se complexifier ! Sinon, prenez votre temps, faites une pause ici, et revenez plus tard 😉

Méthode #2 : contrôle manuel du « hardware SPI », pour aller plus loin

Passons à présent à quelque chose d’un peu plus compliqué ! Ici, nous allons directement manipuler les registres internes du microcontrôleur monté sur la carte Arduino, pour écrire sur le bus SPI avec Arduino. Pour rappel, « écrire sur le bus SPI » est juste une façon de parler que j’utilise, pour dire qu’on va émettre des ordres/consignes/instructions, accompagnés d’éventuelles données, à un périphérique SPI (« esclave »), depuis un arduino (« maître »).

Comme j’utilise un Arduino Nano pour illustrer cet article (cf. chapitre 3), et que celui-ci héberge en son cœur un microcontrôleur ATmega328P, nous allons nous intéresser aux registres internes de ce dernier (comme support, voici un lien vers le datasheet du ATmega328P).

Basiquement, si vous souhaitez prendre le contrôle du bus SPI, vous aurez à faire aux registres SPCR, SPSR, et SPDR du microcontrôleur ATmega328P. Ceux-ci ont les rôles suivants :

  • le registre SPCR (page 140 du datasheet), aussi connu sous le nom de « registre de contrôle SPI », permet d’activer/spécifier certaines caractéristiques de base de la communication SPI (activation ou non du spi en lui-même, activation ou non des interruptions, ordre des données, polarité, phase, mode maître ou esclave, et vitesse de transmission)
  • le registre SPSR (page 141 du datasheet), aussi connu sous le nom de « registre de statut SPI », héberge principalement le bit d’interruption (qui s’active à chaque transfert SPI terminé), et un bit pour doubler la vitesse de communication SPI en mode maître, si souhaité
  • le registre SPDR (page 142 du datasheet), aussi connu sous le nom de « registre de données SPI », contiendra les données lues ou à envoyer

Remarque : ne prenez pas peur, en lisant tout ce qui suit ! Car en fait, une fois les registres SPCR et SPSR configurés initialement, envoyer des données par la suite est un vrai jeu d’enfant !

Alors, pour mieux visualiser ces registres, je vais vous les présenter un par un, avec les bits qui les composent. En commençant par le registre de contrôle SPCR :

Registre SPCR du ATmega328P, registre de contrôle SPI avec bits de sélection et autorisation des paramètres de communication série

Ce registre SPCR est de loin celui qui contient le plus d’infos à l’intérieur. Mais encore une fois, rassurez-vous, car dès que vous aurez grossièrement compris à quoi correspond chaque bit, et que vous aurez paramétré ce registre, le reste est plutôt simple à comprendre 😉

À présent, continuons avec le registre de statut SPSR :

Registre SPSR de l'ATmega328P, registre de statut SPI arduino, avec bit drapeau d'interruption SPIF et bit de sélection vitesse SPI2X, avec SPR0 et SPR1

Si vous occultez WCOL, qui est un cas très particulier, seuls les bits SPIF et SPI2X nous intéressent ici. En notant au passage que SPIF est un « drapeau d’interruption » qui s’active dès qu’un transfert SPI est terminé, et que SPI2X, quant à lui, est un paramètre qu’on règle avant tout.

D’ailleurs, en parlant de SPI2X, sachez que la vitesse de communication sur le bus SPI est déterminée par la combinaison des bits SPI2X (registre SPSR) et SPR1/SPR0 (registre SPCR), selon le tableau suivant :

Tableau réglage vitesse SPI sur ATmega328P, avec sélection fréquence par SPI2X SPR1 SPR0, ajustement cadence horloge SCK pour Arduino

Par exemple, si vous souhaitez cadencer votre horloge SPI à 1 MHz sur votre Arduino Uno ou Nano (avec un microcontrôleur tournant à 16 MHz, de base), alors vous devrez mettre les bis SPI2X à 0, SPR1 à 0, et SPR0 à 1. En effet, ainsi, votre horloge SCK sera cadencée à Fosc / 16 = 16 MHz / 16, soit 1 MHz.

Du reste, voici le registre de données SPDR, qui ne contient au final que les données lues ou à envoyer (selon le cas) :

Registre SPDR ATmega328P, contenant données à lire ou écriture sur bus SPI, avec registre à décalage pour lecture et écriture de transmission série

Voilà pour les registres ! Maintenant, laissez-moi vous montrer comment cela peut se manipuler en pratique !

Pour ce faire, nous allons reprendre notre montage précédent ; pour rappel, celui-ci consistait à envoyer une valeur précise au convertisseur numérique-analogique (DAC), afin qu’il ajuste sa tension de sortie analogique en conséquence. Le tout, piloté par potentiomètre, lui-même branché sur une des entrées analogiques de l’Arduino.

Voici le code de programme, version « hardware SPI », comme j’aime la nommer !

/*
   ______               _                  _///_ _           _                   _
  /   _  \             (_)                |  ___| |         | |                 (_)
  |  [_|  |__  ___  ___ _  ___  _ __      | |__ | | ___  ___| |_ _ __ ___  _ __  _  ___  _   _  ___
  |   ___/ _ \| __|| __| |/ _ \| '_ \_____|  __|| |/ _ \/  _|  _| '__/   \| '_ \| |/   \| | | |/ _ \
  |  |  | ( ) |__ ||__ | | ( ) | | | |____| |__ | |  __/| (_| |_| | | (_) | | | | | (_) | |_| |  __/
  \__|   \__,_|___||___|_|\___/|_| [_|    \____/|_|\___|\____\__\_|  \___/|_| |_|_|\__  |\__,_|\___|
                                                                                      | |
                                                                                      \_|
  Fichier :       prgArduino-TestDAC-2-HardwareSPI.ino

  Description :   Programme permettant de contrôler notre DAC 12 bits, en utilisant les registres SPI de l'Arduino

  Remarques :     --> Méthode utilisée : hardware SPI

  Auteur :        Jérôme TOMSKI (https://passionelectronique.fr/)
  Créé le :       24.04.2023

*/

// Lignes SPI de l'Arduino Nano <--> DAC
#define pin_MOSI          11    // Le signal MOSI sort sur la pin D11 de la carte Arduino (cette pin est reliée en interne à la broche PB3 du µC ATmega328P)
#define pin_SCK           13    // Le signal SCK sort sur la pin D13 de la carte Arduino (cette pin est reliée en interne à la broche PB5 du µC ATmega328P)
#define pin_SS_DAC        3     // Le Slave-Select du DAC sort sur la pin D3 de la carte Arduino (cette pin est reliée en interne à la broche PD3 du µC ATmega328P)
#define pin_SS_spi_master 10    // Broche SS du SPI "de base" de l'Arduino ; elle sera déclarée en "sortie", afin de permettre d'activer le mode "SPI Maître"
#define pin_POTENTIOMETRE A0    // La tension présente sur A0 (modifiable via le potentiomètreà, sera envoyée au DAC, pour "reproduction" en sortie

// Ligne slave-select du DAC (branchée sur sortie D3 de la carte Arduino, soit la pin PD3 du µC, donc le bit 3 sur PORTD[7..0])
#define selectionner_DAC        PORTD &= 0b11110111
#define desactiver_DAC          PORTD |= 0b00001000


// ====================================
// Fonction SETUP (démarrage programme)
// ====================================
void setup() {
  pinMode(pin_SS_DAC, OUTPUT);          // Défini la broche arduino comme sortie, pilotant l'entrée /SS du DAC
  desactiver_DAC;                       // ... et la fixe à l'état haut (communication SPI/DAC désactivée)

  pinMode(pin_MOSI, OUTPUT);      digitalWrite(pin_MOSI, LOW);          // Mise à l'état bas de la ligne MOSI (ligne de données maitre vers esclave)
  pinMode(pin_SCK, OUTPUT);       digitalWrite(pin_SCK, LOW);           // Mise à l'état bas de la ligne SCK (horloge SPI)

  pinMode(pin_SS_spi_master, OUTPUT);   // Note (très importante) : avec le microcontrôleur ATmega328P, même si vous restez "libre" d'utiliser les broches que
                                        // vous souhaitez pour le chip_select/slave_select du SPI, il n'en reste pas moins que la broche D10 a un impact sur
                                        // le mode de fonctionnement SPI (maître ou esclave, j'entends) ; en effet, si vous passez la broche D10 en entrée,
                                        // alors le µC effacera le bit MSTR (qui définit le mode maître/esclave), et passera automatiquement en mode "slave SPI".
                                        // C'est pourquoi il vaut "mieux" utiliser la broche D10 comme première ligne de sélection SPI, pour être sûr d'être en
                                        // "SPI mode master" (sinon vous risquez fort d'avoir des soucis de fonctionnement, sans vraiment comprendre pourquoi !)

  // Configuration du registre SPCR ("SPI Control Register") de l'ATmega328P -> page 140/294 du datasheet
  bitClear(SPCR, SPIE);     // Désactive les interruptions SPI
  bitClear(SPCR, DORD);     // Définit l'ordre des données (ici, à 0, c'est le MSB qui sera passé en premier, jusqu'à finir par le LSB)
  bitSet(SPCR, MSTR);       // Définit l'Arduino en "maître SPI" (d'où ce bit à 1)
  bitClear(SPCR, CPOL);     // Définit la polarité d'horloge SCK (ici, avec la valeur 0, on dit que SCK est actif à l'état haut / inactif à l'état bas)
  bitClear(SPCR, CPHA);     // Définit la phase d'horloge SCK (ici, avec la valeur 0, on dit que les données SPI sont échantillonnées sur chaque début de front d'horloge SCK)
  bitSet(SPCR, SPE);        // Autorise les opérations sur le bus SPI

  bitClear(SPCR, SPR1);     // Si SPR1=0 et SPR0=1, et que SPI2X=0, alors la fréquence d'horloge sur SCK sera égale à Fosc/16 (soit 16 MHz/16, soit 1 MHz)
  bitSet(SPCR, SPR0);       // Nota : c'est juste un exemple de paramétrage ; le DAC utilisé ici peut rouler jusqu'à 20 MHz, sans problème !
  
  // Configuration du registre SPSR ("SPI Status Register") de l'ATmega328P -> page 141/294 du datasheet
  bitClear(SPSR, SPI2X);    // Permet de "doubler" ou non la vitesse d'horloge SCK, de notre Arduino
                            // Nota : ce bit, "SPI2X", sera utilisé en combinaison avec les bits SPR1 et SPR0 (cf. ci-dessus/dessous)
  

          /* Relation entre vitesse d'horloge et bits SPI2X, SPR1, et SPR0
             Nota : dans le cas de notre Arduino Nano, cadencé par un quartz à 16 Mhz, alors Fosc = 16 000 000
 
              ------------------------------------------------------
              | SPI2X |  SPR1 |  SPR0 | Fréquence d'horloge SCK    |
              ------------------------------------------------------
              |   0   |   0   |   0   | Fosc / 4           (4 MHz) |
              |   0   |   0   |   1   | Fosc / 16          (1 MHz) |     <--- fréquence de fonctionnement choisie, pour exemple
              |   0   |   1   |   0   | Fosc / 64        (250 kHz) |
              |   0   |   1   |   1   | Fosc / 128       (125 MHz) |
              |   1   |   0   |   0   | Fosc / 2           (8 MHz) |
              |   1   |   0   |   1   | Fosc / 8           (2 MHz) |
              |   1   |   1   |   0   | Fosc / 32        (500 KHz) |
              |   1   |   1   |   1   | Fosc / 64        (250 KHz) |
              ------------------------------------------------------ 
          */


  // Initialise la communication série, avec le PC
  Serial.begin(9600);
  Serial.println(F("================================================================================"));
  Serial.println(F("Test du DAC modèle MCP 4921, de chez Microchip"));
  Serial.println(F("- Méthode de communication : harware SPI, purement logiciel"));
  Serial.println(F("================================================================================"));
  Serial.println("");

}


// ===========================================================
// Fonction permettant d'écrire un octet sur le bus SPI
// ===========================================================
void ecritureOctetSurBusSPI(uint8_t octetAenvoyer) {

    SPDR = octetAenvoyer;       // Le fait d'enregistrer une valeur dans ce regisre démarre l'échange SPI (on envoie les 8 bits, l'un après l'autre)
    while(!(SPSR & (1<<SPIF))); // Attente de la fin de transmission
    return SPDR;
    
}


// ===========================================================
// Fonction LOOP (boucle programme, après setup)
// ===========================================================
void loop() {

  uint16_t valeurPOT = 0;

  // Lecture valeur d'entrée arduino, sur 10 bits (0..1023), et conversion en valeur 12 bits (0..4095)
  valeurPOT = analogRead(pin_POTENTIOMETRE);
  valeurPOT = map(valeurPOT, 0, 1023, 0, 4095);

  // Affichage de cette valeur sur le moniteur série de l'IDE Arduino
  Serial.print(F("Valeur de l'entrée A0 (potentiomètre) = "));
  Serial.println(valeurPOT);

  // Activation de la communication avec le DAC (en abaissant sa ligne /SS)
  selectionner_DAC;

  // Ajout de 4 bits de configuration du DAC, "à gauche" des 12 bits de données à envoyer
      // 1er bit de config : toujours à 0
      // 2ème bit de config : 0=unbuffered, 1=buffered (ici choix "unbuffured", donc 0)
      // 3ème bit de config : 0=gain double, 1=pas de gain (ici choix "pas de gain", donc 1)
      // 4ème bit de config : 0=sortie DAC éteinte, 1=sortie DAC active (ici choix "sortie du DAC active", donc 1)
      // ... d'où le masque "0011" qui sera rajouté "devant" les 12 bits de poids faible (conservés avec les "000000000000" qui suivent)
  valeurPOT |= 0b0011000000000000;      // On rajoute donc "0011" devant, en utilisant un "OU logique" (symbole : |=)

  // Ecriture de ces 16 bits (4 bits de configuration suivis de 12 bits de données, en 2 x 8 bits, du fait de la taille 8 bits du registre SPI)
  ecritureOctetSurBusSPI((uint8_t)(valeurPOT >> 8));                   // On récupère les 8 bits de poids fort (en les décalant à droite)
  ecritureOctetSurBusSPI((uint8_t)(valeurPOT & 0b0000000011111111));   // On récupère les 8 bits de poids faible (en "effaçant" les autres)

  // Désactivation de la communication avec le DAC (en remontant sa ligne /SS)
  desactiver_DAC;
 
  // Petit délai avant de reboucler, à l'infini
  delay(1000);
}

Comme vous pouvez le constater, une fois les registres SPI initialisés dans la partie « setup », le fait d’écrire sur le bus SPI est vraiment simple. En effet, tout se passe dans la partie « loop », où :

  • la valeur du potentiomètre est lue (sur 10 bits, « imposé » par le µC ATmega328P)
  • cette valeur est convertie sur 12 bits (pour être « conforme » à ce qu’attend notre DAC 12 bits)
  • les 4 bits de configuration sont ajoutés en amont des 12 bits de données (on obtient donc ici un mot de 16 bits)
  • et ce mot de 16 bits est envoyé en 2 octets de 8 bits (du fait que le registre de données SPI de l’ATmega328P de l’Arduino Nano ne fait 8 bits « seulement »)

Si tout se passe bien, vous devriez obtenir une tension fixe analogique en sortie de DAC, comprise entre 0 et +5V, selon de combien est tourné le potentiomètre.

Côté moniteur série, si cela vous intéresse, vous pourrez visualiser les valeurs lues sur l’entrée analogique de l’Arduino (là où est branché le potentiomètre), converti au passage sur 12 bits (0…4095, en décimal), comme requis par le DAC.

Moniteur série test convertisseur numérique analogique arduino via bus SPI, programme utilisant hardware spi de l'ATmega328P dans code

Maintenant que nous avons vu cette deuxième méthode, il nous reste plus qu’à voir la 3ème manière d’envoyer des données en SPI. Alors allons-y 😉

Méthode #3 : bit banging SPI Arduino, pour une maîtrise totale de la communication

Nous arrivons ici à la partie la plus complexe et simple à la fois, car c’est la plus rudimentaire qui soit. En effet, nous allons simplement envoyer des 0 ou des 1 à un rythme donné (ce qu’on appelle le « bit-banging »), pour émuler une communication SPI. Ainsi, nous allons manuellement reproduire le protocole SPI.

Ici, au final, la difficulté principale réside en la « maîtrise » du datasheet du périphérique SPI, avec lequel on souhaite communiquer.

Dans le cas de l’exemple pratique qui accompagne cet article (cf. paragraphe §3), vous verrez que faire du bit banging (c’est à dire envoyer les bits 1 par 1, à la suite les uns des autres) est assez simple. Car pour rappel, concernant notre DAC (convertisseur numérique → analogique), nous devons envoyer 4 bits de configuration suivi de 12 bits de données, via le bus SPI, pour que le DAC fixe sa tension de sortie telle qu’on le souhaite. Soit 16 bits à la queuleuleu 😉

Remarque : vous verrez qu’envoyer 16 bits à la suite en « bit banging » n’a rien de sorcier. Par contre, selon avec quel périphérique SPI vous comptez communiquer à l’avenir, cela pourrait se révéler bien plus fastidieux. En effet, nous avons là qu’une seule commande/ordre à envoyer ; et la taille du message fait toujours 16 bits (4 pour la config, et 12 pour les données). Mais si vous avez plusieurs consignes + données variables à envoyer, vous aurez besoin de bien maîtriser le datasheet du composant visé, avant de coder tout cela ! Cela étant dit, la méthode du bit-banging est un incontournable, notamment si vous travaillez avec un microcontrôleur dépourvu de port/registres SPI !

Sans plus attendre, passons à présent au code de programmation arduino de cette 3ème méthode de communication SPI :

/*
   ______               _                  _///_ _           _                   _
  /   _  \             (_)                |  ___| |         | |                 (_)
  |  [_|  |__  ___  ___ _  ___  _ __      | |__ | | ___  ___| |_ _ __ ___  _ __  _  ___  _   _  ___
  |   ___/ _ \| __|| __| |/ _ \| '_ \_____|  __|| |/ _ \/  _|  _| '__/   \| '_ \| |/   \| | | |/ _ \
  |  |  | ( ) |__ ||__ | | ( ) | | | |____| |__ | |  __/| (_| |_| | | (_) | | | | | (_) | |_| |  __/
  \__|   \__,_|___||___|_|\___/|_| [_|    \____/|_|\___|\____\__\_|  \___/|_| |_|_|\__  |\__,_|\___|
                                                                                      | |
                                                                                      \_|
  Fichier :       prgArduino-TestDAC-3-BitBanging.ino

  Description :   Programme permettant d'envoyer des consignes à un DAC 12 bits, en simulant le protocole SPI

  Remarques :     --> Méthode utilisée : bit banging

  Auteur :        Jérôme TOMSKI (https://passionelectronique.fr/)
  Créé le :       24.04.2023

*/

// Lignes SPI de l'Arduino Nano <--> DAC
#define pin_MOSI          11    // Le signal MOSI sort sur la pin D11 de la carte Arduino (cette pin est reliée en interne à la broche PB3 du µC ATmega328P)
#define pin_SCK           13    // Le signal SCK sort sur la pin D13 de la carte Arduino (cette pin est reliée en interne à la broche PB5 du µC ATmega328P)
#define pin_SS_DAC        3     // Le Slave-Select du DAC sort sur la pin D3 de la carte Arduino (cette pin est reliée en interne à la broche PD3 du µC ATmega328P)
#define pin_POTENTIOMETRE A0    // La tension présente sur A0 (ajustable via potentiomètre) sera envoyée au DAC, pour "reproduction" sur sa sortie analogique

// Ligne slave-select du DAC (branchée sur sortie D3 de la carte Arduino, soit la pin PD3 du µC, donc le bit 3 sur PORTD[7..0])
#define selectionner_DAC        PORTD &= 0b11110111
#define desactiver_DAC          PORTD |= 0b00001000

// Ligne MOSI (branchée sur sortie D11 de la carte Arduino, soit la pin PB3 du µC, donc le bit 3 sur PORTB[7..0])
#define mettre_MOSI_a_etat_bas  PORTB &= 0b11110111
#define mettre_MOSI_a_etat_haut PORTB |= 0b00001000

// Ligne SCK (branchée sur sortie D13 de la carte Arduino, soit la pin PB5 du µC, donc le bit 5 sur PORTB[7..0])
#define mettre_SCK_a_etat_bas   PORTB &= 0b11011111
#define mettre_SCK_a_etat_haut  PORTB |= 0b00100000


// ===========================================================
// Fonction SETUP (démarrage programme)
// ===========================================================
void setup() {
  
  // Désactivation de la communication SPI/DAC pour commencer, au démarrage
  pinMode(pin_SS_DAC, OUTPUT);          // Défini cette broche comme sortie (raccordée à l'entrée SS du DAC)
  digitalWrite(pin_SS_DAC, HIGH);       // ... et la fixe à l'état haut (pour désactiver la communication SPI/DAC, pour l'instant)

  // Mise au repos du bus SPI
  pinMode(pin_MOSI, OUTPUT);      digitalWrite(pin_MOSI, LOW);          // Mise à l'état bas de la ligne MOSI (ligne de données "maitre" vers "esclave")
  pinMode(pin_SCK, OUTPUT);       digitalWrite(pin_SCK, LOW);           // Mise à l'état bas de la ligne SCK (horloge SPI)
  
  // Initialisation de la communication série, avec le PC
  Serial.begin(9600);
  Serial.println(F("================================================================================"));
  Serial.println(F("Test du DAC modèle MCP 4921, de chez Microchip"));
  Serial.println(F("- Méthode de communication : bit-banging SPI, purement logiciel"));
  Serial.println(F("================================================================================"));
  Serial.println("");
  
}


// =================================================
// Fonction permettant d'écrire 1 bit sur le bus SPI
// =================================================
void ecritureBitSPI(uint16_t valeur) {
  valeur == 0 ? mettre_MOSI_a_etat_bas : mettre_MOSI_a_etat_haut; 
  mettre_SCK_a_etat_haut;
  mettre_SCK_a_etat_bas;

  // Nota : à 16 MHz, chaque cycle du µC dure 62,5 ns (bien qu'on soit à la vitesse maximale,
  //        c'est encore suffisamment "lent" pour ce DAC, qui peut fonctionner jusqu'à 20 MHz)
}


// ==============================================
// Fonction LOOP (boucle principale, après setup)
// ==============================================
void loop() {
  uint16_t valeurPOT = 0;

  // Lecture valeur d'entrée arduino, sur 10 bits (0..1023), et conversion sur 12 bits (0..4095)
  valeurPOT = analogRead(pin_POTENTIOMETRE);
  valeurPOT = map(valeurPOT, 0, 1023, 0, 4095);

  // Affichage de cette valeur sur le moniteur série
  Serial.print(F("Valeur de l'entrée A0 (potentiomètre) = "));
  Serial.println(valeurPOT);

  // Activation de la communication avec le DAC (en abaissant sa ligne /SS)
  selectionner_DAC;

  // *************************************************************************************************
  // Envoi des 16 bits nécessaires au DAC (pour rappel : 4 bits de configuration + 12 bits de données)
  // *************************************************************************************************

  // Ecriture des 4 bits de configuration du DAC
  ecritureBitSPI(0);     // 1er bit de config : toujours à 0
  ecritureBitSPI(0);     // 2ème bit de config : 0=unbuffered, 1=buffered (ici choix "unbuffured", donc 0)
  ecritureBitSPI(1);     // 3ème bit de config : 0=gain double, 1=pas de gain (ici choix "pas de gain", donc 1)
  ecritureBitSPI(1);     // 4ème bit de config : 0=sortie DAC éteinte, 1=sortie DAC active (ici choix "sortie active", donc 1)

  // Ecriture des 12 bits de données
  ecritureBitSPI(valeurPOT & (1 << 11));    // Bit 11
  ecritureBitSPI(valeurPOT & (1 << 10));    // Bit 10
  ecritureBitSPI(valeurPOT & (1 << 9));     // Bit 9
  ecritureBitSPI(valeurPOT & (1 << 8));     // Bit 8
  ecritureBitSPI(valeurPOT & (1 << 7));     // Bit 7
  ecritureBitSPI(valeurPOT & (1 << 6));     // Bit 6
  ecritureBitSPI(valeurPOT & (1 << 5));     // Bit 5
  ecritureBitSPI(valeurPOT & (1 << 4));     // Bit 4
  ecritureBitSPI(valeurPOT & (1 << 3));     // Bit 3
  ecritureBitSPI(valeurPOT & (1 << 2));     // Bit 2
  ecritureBitSPI(valeurPOT & (1 << 1));     // Bit 1
  ecritureBitSPI(valeurPOT & (1 << 0));     // Bit 0


  // Désactivation de la communication avec le DAC (en remontant sa ligne /SS)
  desactiver_DAC;

  delay(1000);   // Petit délai
}

Petite note importante, concernant ce programme : ici, nous envoyons les bits les uns après les autres, sans se soucier de la régularité, ni de la vitesse d’envoi (qui reste élevée, mais acceptable !). Car le DAC utilisé peut fonctionner jusqu’à 20 MHz, et l’Arduino Nano est limité à 16 MHz ; du coup, même à vitesse maximale sur le bus SPI (horloge SCK à Fosc/2, soit 8 MHz), nous sommes bien en dessous de ce que peut support le DAC !

Comme vu dans les exemples précédents, j’ai inscris bon nombre de #define, en début de code ; pour rappel, cette instruction permet de substituer du code ou une valeur, à son étiquette. Ainsi le code est allégé, et bien plus clair. Du coup, par exemple :

  • quand vous verrez « #define pin_MOSI 11 » écrit, cela signifie que toutes les étiquettes nommées « pin_MOSI » seront remplacées par la valeur « 11 »
  • quand vous verrez « #define selectionner_DAC PORTD &= 0b11110111 » écrit, cela signifie que l’opération « PORTD =  PORTD & 0b11110111 » (ET logique) remplacera chaque mention « selectionner_DAC » écrite dans le programme
  • quand vous verrez « #define mettre_SCK_a_etat_haut PORTB |= 0b00100000 » écrit, cela signifie que l’opération « PORTB = PORTB | 0b00100000 » (OU logique) se substituera à chaque étiquette « mettre_SCK_a_etat_haut »

En espérant que tout cela ne vous déroute pas trop, si vous n’en avez pas l’habitude !

Remarque : j’ai utilisé les broches D3, D11, et D13 pour émuler cette écriture SPI ; mais bien évidemment, vous êtes libres de choisir d’autres broches, à votre convenance !

Du reste, une fois le programme uploadé, la sortie du DAC devrait délivrer une tension comprise entre 0 et 5V, selon de combien est tourné le potentiomètre. Si besoin, là aussi, n’hésitez pas à ouvrir le moniteur série de l’IDE Arduino, pour jeter un coup d’œil aux valeurs lues sur le potentiomètres, converties au format 12 bits (0 à 4095), pour savoir ce qui est envoyé au DAC, via le bus SPI.

Bit banging SPI Arduino, exemple de communication entre microcontrôleur et DAC de 12 bits sur bus série, code moniteur série de test

Voilà ! En espérant ne pas vous avoir perdus 😉

Écriture SPI Arduino : conclusion !

J’espère que cet article vous aura plu, et pas trop rebuté ! Si c’est le cas, dites-vous que cela pourra vous servir plus tard, si jamais vous ne trouvez pas de librairie Arduino correspondant à vos besoins, et si vous souhaitez simplement prendre pleinement le contrôle de la communication SPI 😉

Aussi, comme évoqué en intro, cet article sur « comment écrire sur le bus SPI avec Arduino » sera complété par un autre, abordant cette fois-ci la « lecture SPI ». Ainsi, vous aurez une vue complète de ce qui peut être fait, de manières différentes de d’habitude !

Sur ce, je vous laisse ! À très bientôt !
Jérôme.

À découvrir aussi : plus globalement, les liaisons séries filaires (I2C, SPI, et UART)

Ce contenu vous plaît ? Alors abonnez-vous à la Newsletter pour ne rien louper !

(*) Mis à jour le 01/12/2023

11 commentaires sur “3 façons d’écrire sur le bus SPI avec Arduino (via librairie SPI.h, en faisant du « hardware SPI », ou du « bit banging »)”

  1. Site passionelectronique.fr

    Bonsoir,

    Une phrase qui se termine un peu rapidement après l’explication de l’horloge SCK (votre horloge sera …) :

    Par exemple, si vous souhaitez cadencer votre horloge SPI à 1 MHz sur votre Arduino Uno ou Nano (avec un microcontrôleur tournant à 16 MHz, de base), alors vous devrez mettre les bis SPI2X à 0, SPR1 à 0, et SPR0 à 1. En effet, ainsi, votre horloge SCK sera

    Du reste, voici le registre de données SPDR, qui ne contient au final que les données lues ou à envoyer (selon le cas)

  2. Site passionelectronique.fr

    Bonsoir,

    Excellent article.
    Clair et net, sans prise de tête.

    Petite correction à faire en §6 : « Nous arrivons ici à la complexe et simple à la fois,… », à corriger en (?) : « Nous arrivons ici à la partie la plus complexe et simple à la fois,… »

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Symbole danger point d'exclamation site PSE, triangle jaune avec contour noir, texte noir alerte au milieuAfin de filtrer au maximum les messages de type "spam" ou "inappropriés", chaque commentaire est soumis à modération, et validé manuellement. Du coup, il se peut que certains commentaires ne soient pas publiés, ou sinon, avec un peu de retard. Par ailleurs, j'ai malheureusement plus de messages à traiter que de temps pour y répondre ; c'est pourquoi je ne pourrais pas répondre à tout le monde. Désolé …