Machins de dev

Contrôler les LEDs de l’APU (PC Engines)

Depuis 2 ans déjà, j’utilise pour mon accès à Internet personnel non pas une de ces box merdiques, moches, bancales (hello Freebox v6), obscures (black box) et pleines de vide (coucou Freebox v6) mais un APU de PC Engines. C’est une carte mère disposant d’un processeur intégré AMD à architecture amd64, 3 ports Ethernet et 3 leds accessibles via GPIO, donc grosso-modo un mini-pc à consommation très réduite. Ce truc est génial.

Par défaut la première LED indique simplement si le routeur est alimenté ou non et les deux autres leds sont éteintes. Je me suis enfin décidé à me pencher dessus afin de m’en servir pour afficher des indicateurs plus utiles tels que la charge réseau ou la charge cpu.

Le noyau Linux, actuellement en version 4.4.3 au moment où j’écris ces lignes, ne dispose pas de pilotes LED ou GPIO permettant d’accéder à ces LEDS depuis le user-space. Cependant Daduke a développé un petit module linux pour combler ce petit manque.

Après une correction mineure sur les sources, probablement du à une évolution des interfaces du noyau depuis, le module compile bien, se charge bien et permet bien d’accéder aux leds. Ceux-ci sont rendus accessibles par le kernel au userspace dans le dossier /sys/class/leds/.

Une fois que les leds sont accessibles, il suffit de les contrôler manuellement en envoyant 0 ou 255 à /sys/class/leds/{your_led}/brightness  comme suit :

# switch off the first led
echo 0 > /sys/class/leds/apu\:1/brightness

# swich on the first led
echo 255 > /sys/class/leds/apu\:1/brightness

Il est également possible d’utiliser un déclencheur géré directement par le kernel, ce qui est bien plus efficace que d’utiliser un processus en user-space si le trigger existe déjà dans le kernel :

# list available triggers on the first led
# the current trigger is in brackets
cat /sys/class/leds/apu\:1/trigger

# set the trigger heartbeat on the first led
echo heartbeat > /sys/class/leds/apu\:1/trigger

Génération du package Gentoo

L’écriture du package se découpe en 3 parties :

Maintenant que l’on a bien vérifié que ça marche, il est temps de packager afin de faciliter les mises à jour du noyau et éviter de crasser le système avec un vieux make install en dehors de tout contrôle du système de packages de la distribution.

Tout d’abord le Makefile : doit-on l’écrire, doit-on passer par automake, aclocal, autoscan, autoconf, tous ces outils ensembles ? Je n’ai pu trouver qu’un seul how-to sur Internet indiquant comment créer un configure en charge de générer le Makefile pour un module kernel utilisant ces outils, qui pour rappels, permettent entre autre de procéder à un certain nombre de vérification sur le système, la présence de librairies, et générer un Makefile compatible avec le système.
Le tutoriel ne fonctionne pas, indiquant à autoconf une instruction non reconnue.
Vu que tout ces outils semblent un peu trop usine à gaz, je décide de laisser tomber pour le moment et de me contenter du Makefile d’origine.

Ensuite c’est l’écriture de l’ebuild. Celui-ci n’a pas été trop difficile, la documentation des eclass étant plutôt complète et il existe des ebuilds sur lequel on peut s’appuyer comme net-firewall/ipset ou net-firewall/xtables-addons. Cela donne l’ebuild qui suit (cet ebuild compile 2 modules et non un seul, qu’on va voir dans la section suivante) :

# Copyright 1999-2016 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
# $Id$

EAPI=5
inherit autotools linux-info linux-mod

DESCRIPTION="Kernel modules to access to leds on the apu motherboard"
HOMEPAGE="https://blog.kveer.fr"
SRC_URI=""

LICENSE="GPL-2"
SLOT="0"
KEYWORDS="amd64"
IUSE=""

DEPEND=""
RDEPEND="${DEPEND}"

MODULE_NAMES='leds-apu(kernel/drivers/leds) apu-btn-gpio(kernel/drivers/gpio)'

pkg_setup() {
    kernel_is -lt 4 3 3 && die "${PN} requires kernel greater than 4.3.3."

    CONFIG_CHECK='LEDS_CLASS MODULES'
    ERROR_LEDS_CLASS='leds-apu requires LEDS_CLASS support in your kernel.'
    ERROR_MODULES='Nonmodular kernel detected. Build modular kernel.'

    linux-mod_pkg_setup
}

src_unpack() {
    mkdir "$S"
    cp "${FILESDIR}"/* "${S}/"
}

src_compile() {
    set_arch_to_kernel
    #KERNEL_DIR=$KERNEL_DIR emake
    BUILD_PARAMS="KERNEL_DIR=$KERNEL_DIR CONFIG_DEBUG_SECTION_MISMATCH=y" linux-mod_src_compile
}

src_install() {
    BUILD_PARAMS="KERNEL_DIR=$KERNEL_DIR" linux-mod_src_install
}

On peut maintenant revenir au Makefile. J’ai trouvé une bonne référence indiquant comment fonctionne les scripts de compilation du kernel et comment de fait écrire un Makefile. Le problème rencontré avec le Makefile de Daduke est qu’il n’est capable de compiler qu’un seul module, or j’en avais deux. Cela m’a amené à créer deux fichiers:

PWD = $(shell pwd)
KDIR = $(KERNEL_DIR)

modules module:
        $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules

install: $(MODULE)
        $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules_install

clean:
        $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) clean
obj-m := leds-apu.o apu-btn-gpio.o
leds-apu.y := leds-apu.c
apu-btn-gpio.y := apu-btn-gpio.c

Configurer les LEDs de l’apu

J’ai configuré mes leds comme suit:

Je paramètre les leds au démarrage du routeur avec ce petit script :

#!/bin/bash

echo heartbeat > /sys/devices/platform/apu-led/leds/apu\:1/trigger
echo netfilter-lednet-in > /sys/devices/platform/apu-led/leds/apu\:2/trigger
echo netfilter-lednet-out > /sys/devices/platform/apu-led/leds/apu\:3/trigger

J’utilise iptables pour déclencher le bip sur les paquets entrants ou sortants (ces deux règles sont un peu bidon et ne servent d’exemple qu’à l’utilisation de la cible LED, il faudra procéder de manière similaire pour capturer des trames IPv6 ou autre avec ebtables) :

iptables -A PREROUTING -i enp1s0 -j LED --led-trigger-id "lednet-in" --led-delay 10 --led-always-blink
iptables -A POSTROUTING -o enp1s0 -j LED --led-trigger-id "lednet-out" --led-delay 10 --led-always-blink

Enfin, on n’oubliera pas de charger les modules au démarrage en modifiant le fichier /etc/conf.d/modules :

modules='led_class ledtrig-heartbeat ledtrig-oneshot ledtrig-timer leds-apu'

N’hésitez pas à me laisser vos script en commentaires.

Amélioration du code du module

Avant de considérer le package comme terminé, j’ai activé tous les warning à gcc en ajoutant les flags -Wall CONFIG_DEBUG_SECTION_MISMATCH=y  et compilé le module, dans le but de corriger tous les warning relevés.  Et c’est là, c’est la merde…

Le principal warning est Section mismatch in reference from the function ma_function() to the variable ma_variable() . De ce que j’ai pu comprendre, cet avertissement indique qu’une fonction fait appel à une variable qui se trouve en dehors de sa section, donc potentiellement qui n’existe plus ou pas encore (donc un pointer stale).

C’est la merde parce que une fois l’erreur comprise, on lit le code source, on cherche à savoir comment ce type d’erreur se résout et on se rend compte que la documentation concernant les fonctions du kernel Linux et la manière de bien écrire un module est très morcelée. Je soulèverai notamment que :

Franchement non seulement ça ne facilite en rien la maîtrise de l’infrastructure du noyau (et pour ma part cela ne concernait que la partie concernant les modules linux et les IO bruts, c’est à dire une infime partie du noyau), mais ça en devient même frustrant et retire pour ma part tout volonté à y apporter ma contribution (déjà que débugger du C sous Linux c’est une tanné, alors si en plus c’est en mode kernel avec une doc de merde…).

Fin

A mon sens et contrairement à d’autres projets utilisant des langages plus structurés (oui le C c’est quand même bien dégueulasse), il est absolument nécessaire d’avoir une documentation claire indiquant précisant l’impact de certaines macros telles que module_init, module_exit, la différence avec la macro module_platform_driver, l’impact du nom du driver, les différents moyens de déclarer de la mémoire, le mécanisme permettant de la mémoire privée pour un pilote, etc… J’ai dû découvrir tous ces éléments par essais successifs, ce qui n’est pas une méthode d’apprentissage très efficace. En l’état actuel, le développement dans le noyau Linux est pour ma part un no go.

Au final, je n’ai pu résoudre les SECTIONS MISMATCH. Cependant j’ai séparé le module initial de Daduke en 2, l’un concernant uniquement le bouton reset (qui se trouve sur la carte mère), et l’autre concernant le contrôle des LEDs. Le code source pourrait être factorisé mais après avoir passé une nuit à comprendre le fonctionnement d’un module Linux, je n’ai plus le courage de faire ce dev et perdre tous mes cheveux lors du débogage.

Je livre néanmoins le package permettant de compiler le module avec Gentoo, qui se trouve sur mon repo perso. Au moins, mes leds clignotent dans tous les sens, et ça c’est sympa #sapin-de-noel.