---
title: "Utiliser les écrans à encre électronique Waveshare sans Raspberry Pi"
date: !!timestamp '2023-08-02 11:37:00'
image: /post/waveshare-e-paper-display-on-linux-without-raspberry-pi/og.webp
tags:
- linux
- e-paper
- kernel
- spi
aliases:
- waveshare
---

En termes d'écrans à encre électronique (e-ink), Waveshare est un des rares constructeurs permettant d'acheter des écrans de toute taille.
Annoncés compatibles ESP32, Arduino et Raspberry Pi, ils sont en fait compatibles avec n'importe quelle carte de développement exposant le protocole SPI.

Les Raspberry Pi était devenues difficiles à trouver ces derniers mois, nous allons voir dans cet article comment utiliser une autre carte sous Linux pour utiliser un écran Waveshare.

<!-- more -->

## Le protocole de communication de l'écran

Les écrans à encre électronique de Waveshare sont pilotés par le protocole SPI.
Au même titre que l'USB ou l'I²C, il s'agit d'un standard courant de communication entre une machine et un périphérique.

Pour communiquer, le protocole utilise 3 fils + 1 fil par périphérique :

- `SCLK` (ou `SCK` ou `SCL`) est le signal d'horloge pour synchroniser la communication ;
- `MOSI` (ou `SDO` ou `SDA`) est le flux de données allant de la machine vers le périphérique ;
- `MISO` (ou `SDI`) est le flux de données allant du périphérique vers la machine.

Plusieurs périphériques peuvent partager le bus SPI, les 3 fils décrits ci-dessus.
Pour savoir à qui sont destinées les informations, il y a donc un fil supplémentaire pour chaque périphérique.
Avec 3 périphériques sur le bus SPI, on aura donc 6 fils : les 3 fils du bus, partagés, et 1 fil par périphérique, non-partagé.

Ce fil supplémentaire, nommé `CS` pour Chip Select (ou `SS`), est alimenté lorsque la machine souhaite parler au périphérique désigné.
1 seul périphérique est actif à la fois pour éviter les collisions sur le bus (imaginer deux périphériques envoyer deux messages distincts en même temps ... sur le même fil).
Un périphérique ne parle et n'écoute que lorsque son `CS` est à l'état bas (à 0).

Lorsqu'un périphérique envoie ou reçoit des données, il se synchronise sur la fréquence d'horloge transmise par la machine sur le fil `SCLK`.
Au top d'horloge, on peut lire 1 bit d'information sur `MISO` et `MOSI`.

La machine et le périphérique peuvent envoyer simultanément des données puisque nous avons 1 fil dédié à l'émission et 1 fil dédié à la réception.
Dans le cas des écrans Waveshare, seule la communication montante vers le périphérique est utilisée, l'écran ne répondra jamais sur le bus SPI.
Il n'y a donc pas de fil pour `MISO`.
Pour communiquer son statut, il utilise des fils dédiés.


## Les autres fils utilisés

En plus de l'alimentation électrique (+3.3 V et la masse) et des fils utilisés pour le protocole SPI, 3 fils viennent compléter la communication :

- `DC` : à 0 lorsque l'on enverra des commandes sur le bus SPI. À 1 lorsque ce sera des données qui y seront envoyées.
- `RST` : lorsque ce fil est à 0, cela provoque le redémarrage de l'écran, la réinitialisation de tous ses registres. On le replace à 1 pour commencer à l'utiliser.
- `BUSY` : la puce contrôlant l'écran utilise ce fil pour indiquer si l'écran ou le contrôleur est occupé, en train de réaliser une action (lorsqu'il est à 0) ou s'il est prêt à recevoir des informations (il est alors à 1) .

Ces spécifications étant bien établies, rien ne nous limite aux seules Raspberry Pi !


## Reconnaître une carte compatible

Pour être utilisée avec l'écran, la carte doit exposer un bus SPI sur ses GPIO, et vous devez disposer de 3 emplacements génériques disponibles en plus du 3,3 V et de la masse.

Toutes les cartes, récentes et anciennes, exposent ces interfaces.

Voici par exemple [la correspondance des broches de la Pine64](https://files.pine64.org/doc/Pine%20A64%20Schematic/Pine%20A64%20Pin%20Assignment%20160119.pdf) :

![Correspondance des broches Pi-2 de la Pine64-LTS](pine64-pi2-pinout.webp)

Et [ceux de la Cubieboard](http://docs.cubieboard.org/cubieboard1_and_cubieboard2_gpio_pin) :

![Correspondance des broches U15 de la Cubieboard](cubieboard-u15-pinout.webp)

Certaines cartes exposent des GPIO identiques à ceux de la Raspberry Pi, vous pourrez donc placer le HAT fourni directement dessus.
Lorsque l'organisation des broches est différente, il faudra câbler manuellement.

Pour ma part, j'ai utilisé une carte MIPS Creator CI 20 :

[![Correspondance des broches de la CI20](ci20-pinout.webp)](https://elinux.org/CI20_Hardware#Primary_expansion_header)

Et voici donc le câblage :

![La CI20 câblée](ci20-attached.webp)


## Configuration Linux

La [documentation de Waveshare](https://www.waveshare.com/wiki/7.5inch_e-Paper_HAT_Manual#Enable_SPI_Interface) propose d'utiliser les utilitaires livrés avec Raspbian.
Nous n'avons pas les mêmes utilitaires simplifiés, nous allons donc voir comment faire directement.

### Activer le SPI

Habituellement, on charge un module noyau pour communiquer avec un appareil relié en SPI.
Ce module créera alors une couche d'abstraction et exposera l'abstraction dans le dossier `/dev`.

Ici, nous n'avons pas de module noyau dédié à l'écran, en fait le programme que l'on va lancer, la démo de Waveshare, communique directement en SPI.
C'est comme si on avait écrit un programme pour lire et écrire sur une clef USB en envoyant les commandes USB brutes, plutôt que de faire en sorte que le noyau affiche le périphérique sous `/dev/sdb`.

Afin de piloter le bus SPI depuis un programme de l'espace utilisateur, on va devoir indiquer à notre noyau qu'il doit utiliser le pilote `spidev` pour gérer le bus en question.

Si vous avez compilé vous-même votre noyau, assurez-vous d'avoir ce module :

```
42sh$ zgrep SPI_DEV /proc/config.gz
CONFIG_SPI_SPIDEV=y
```

Le noyau obtient les informations sur les périphériques et les modules à utiliser en consultant un fichier dit *Device Tree*, propre à chaque SBC.

Généralement, lorsqu'un périphérique n'est pas utilisé, il est marqué comme désactivé dans le *Device Tree*.
Si vous n'avez pas de fichier `/dev/spidev*`, il va donc falloir commencer par l'activer.

### Activation par Device Tree Overlay

L'outil de Raspbian qui active le bus SPI dans la documentation de Waveshare va en fait activer un *Device Tree Overlay* préconçu.

Nous pouvons faire de même en [créant notre propre *Device Tree Overlay*](https://bootlin.com/blog/using-device-tree-overlays-example-on-beaglebone-boards/).
Le but étant d'activer notre bus SPI.

À partir du fichier DTB de base pour notre carte, nous devrions avoir quelque part (potentiellement dans les `#include`) un bloc comme celui-ci :

```c
spi0: spi@10043000 {
	compatible = "ingenic,jz4780-spi";
	reg = <0x10043000 0x1c>;
	#address-cells = <1>;
	#size-cells = <0>;

	interrupt-parent = <&intc>;
	interrupts = <8>;

	clocks = <&cgu JZ4780_CLK_SSI0>;
	clock-names = "spi";

	dmas = <&dma JZ4780_DMA_SSI0_RX 0xffffffff>,
	       <&dma JZ4780_DMA_SSI0_TX 0xffffffff>;
	dma-names = "rx", "tx";

	status = "disabled";
};
```

On constate qu'il est bien désactivé (`status = "disabled"`).

Par rapport à nos broches, s'il s'agit bien de celles que l'on souhaite utiliser, on va alors établir l'*overlay* suivant (`my_spidev.dts`) :

```c
/dts-v1/;
/plugin/;

&spi0 {
    status = "okay";

    spidev0: spidev@0{
		compatible = "spidev";
		reg = <0>;
		#address-cells = <1>;
		#size-cells = <0>;
		spi-max-frequency = <500000>;
	};
}
```

Compilons maintenant notre *overlay* :

```
dtc -O dtb -o MY_SPIDEV.dtbo my_spidev.dts
```

On le place ensuite dans notre partition de démarrage, aux côtés du noyau et de notre `dtb`.
Par exemple :

```
mv MY_SPIDEV.dtbo /boot/overlay/MY_SPIDEV.dtbo
```

Il faut ensuite redémarrer et dans u-boot, charger cet *overlay* :

```
# What is currently done
load mmc 0:1 0x820000000 ci20.dtb
fdt addr 0x82000000

# Steps to add after device tree loading
load mmc 0:1 0x83000000 overlays/MY_SPIDEV.dtbo
fdt resize 8192
fdt apply 0x83000000

# Then boot as usual
bootz ...
```

Une fois que l'on aura testé que cela fonctionne, on pourra l'ajouter au script de démarrage.


### Faire sans bus SPI

Si vous n'avez pas de bus SPI disponible, il reste possible d'utiliser des broches GPIO et de les dédier à cet usage.
On utilisera pour cela le module noyau `spi-gpio`, en plus de `spidev`.

Vérifions d'abord qu'on dispose bien de son support :

```
42sh$ zgrep SPI_GPIO /proc/config.gz
CONFIG_SPI_GPIO=y
```

Voici l'*overlay* que l'on pourrait écrire pour simuler un bus SPI sur les broches 19, 21, 23, 24 et 26 :

```c
/dts-v1/;
/plugin/;

#include <dt-bindings/gpio/gpio.h>

spi_gpio {
    compatible = "spi-gpio";
    #address-cells = <1>;
    #size-cells = <0>;
    gpio-sck = <&gpf 15 GPIO_ACTIVE_HIGH>; /* PE15 */
    gpio-miso = <&gpf 14 GPIO_ACTIVE_HIGH>; /* PE14 */
    gpio-mosi = <&gpf 17 GPIO_ACTIVE_HIGH>; /* PE17 */
    num-chipselects = <2>;
    cs-gpios = <&gpf 16 GPIO_ACTIVE_HIGH &gpf 18 GPIO_ACTIVE_HIGH>; /* PE16, PE18 */

    spidev@0 {
        compatible = "spidev";
        reg = <0>;
        spi-max-frequency = <1000000>;
    };
};
```

On applique l'overlay au démarrage, comme vu précédemment.

Dans les deux cas, il est aussi possible de faire les modifications directement dans le fichier DTS original et de remplacer le DTB utiliser pour démarrer la carte.
Il faudra alors veiller à refaire cela à chaque mise à jour du noyau.


## Configuration des GPIO dédiés

On avait vu que l'écran utilisait 3 GPIO en plus du bus SPI.

Dans mon cas, j'ai choisi d'utiliser les broches 18 (`PF2`), 11 (`PD26`) et 22 (`PE8`).
Déclarons-les avec :

```sh
# BUSY: PF2
echo 162 > /sys/class/gpio/export

# RST: PD26
echo 122 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio122/direction

# DC: PE8
echo 136 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio136/direction
```


## Adapter le code de la démo

La démo de Waveshare s'attend à être exécutée sur un Raspberry Pi et on retrouve donc un certain nombre d'éléments écrits en dur.
De plus, elle fait usage d'une dépendance Python qui ne fonctionne pas en dehors de cette plateforme.

Voici [l'implémentation](https://git.nemunai.re/nemunaire/waveshareteam-e-Paper/commit/dfa74b004198794d01bdc461ea88d1cf9cdb80b5) que j'ai effectué afin d'adapter le code à mon usage :

```diff
--- a/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epdconfig.py
+++ b/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epdconfig.py
@@ -230,6 +230,89 @@ class SunriseX3:
         self.GPIO.cleanup([self.RST_PIN, self.DC_PIN, self.CS_PIN, self.BUSY_PIN], self.PWR_PIN)


+class CreatorCI20:
+    # Pin definition
+    RST_PIN  = 122
+    DC_PIN   = 136
+    CS_PIN   = 0
+    BUSY_PIN = 162
+    PWR_PIN  = 18
+    Flag     = 0
+
+    def __init__(self):
+        import spidev
+
+        self.SPI = spidev.SpiDev()
+
+    def digital_write(self, pin, value):
+        if pin == 0:
+            return
+
+        with open("/sys/class/gpio/gpio"+str(pin)+"/value", "w") as f:
+            f.write(str(value))
+
+    def digital_read(self, pin):
+        v = "0"
+        if pin != 0:
+            with open("/sys/class/gpio/gpio"+str(pin)+"/value") as f:
+                v = f.readline()
+        return int(v)
+
+    def delay_ms(self, delaytime):
+        time.sleep(delaytime / 1000.0)
+
+    def spi_writebyte(self, data):
+        self.SPI.writebytes(data)
+
+    def spi_writebyte2(self, data):
+        # for i in range(len(data)):
+        #     self.SPI.writebytes([data[i]])
+        self.SPI.xfer3(data)
+
+    def module_init(self):
+        if self.Flag == 0:
+            self.Flag = 1
+
+            # BUSY
+            with open("/sys/class/gpio/export", "w") as f:
+                f.write(str(self.BUSY_PIN))
+
+            # RST
+            with open("/sys/class/gpio/export", "w") as f:
+                f.write(str(self.RST_PIN))
+            with open("/sys/class/gpio/gpio" + str(self.RST_PIN) + "/direction", "w") as f:
+                f.write("out")
+
+            # DC
+            with open("/sys/class/gpio/export", "w") as f:
+                f.write(str(self.DC_PIN))
+            with open("/sys/class/gpio/gpio" + str(self.DC_PIN) + "/direction", "w") as f:
+                f.write("out")
+
+            # SPI device, bus = 0, device = 0
+            self.SPI.open(32766, 0)
+            self.SPI.max_speed_hz = 4000000
+            self.SPI.mode = 0b00
+            return 0
+        else:
+            return 0
+
+    def module_exit(self):
+        logger.debug("spi end")
+        self.SPI.close()
+
+        logger.debug("close 5V, Module enters 0 power consumption ...")
+        self.Flag = 0
+
+        self.digital_write(self.RST_PIN, 0)
+        self.digital_write(self.DC_PIN, 0)
+
+        # Clean up
+        for pin in [self.BUSY_PIN, self.RST_PIN, self.DC_PIN]
+        with open("/sys/class/gpio/unexport", "w") as f:
+            f.write(str(pin))
+
+
 if os.path.exists('/sys/bus/platform/drivers/gpiomem-bcm2835'):
     implementation = RaspberryPi()
 elif os.path.exists('/sys/bus/platform/drivers/gpio-x3'):
```

## Conclusion

On ne peut pas s'attendre à ce qu'un constructeur passe du temps à documenter l'usage de son périphérique pour toutes les plateformes existantes.
On remerciera Waveshare pour avoir [mis en ligne des spécifications très détaillées](https://www.waveshare.com/w/upload/6/60/7.5inch_e-Paper_V2_Specification.pdf) qui permettent aux utilisateurs avancés de comprendre le fonctionnement de l'écran et la manière de communiquer avec lui.
