Linux Embarqué Kernel Module Séances 20–21 · 14 min

Linux Kernel —
Character Device
& Driver I2C

Écrire un module kernel Linux depuis zéro — enregistrement d'un character device, interface file_operations, puis driver I2C intégré dans le sous-système Linux.

Architecture kernel Linux

// Les 4 niveaux d'accès hardware sur Linux
Userspace
cat /dev/hello · Python · echo 1 > sysfs
↕ syscalls (open, read, write, ioctl)
VFS — Virtual File System
interface unifiée — tout est fichier
↕ file_operations struct
Notre module kernel (.ko)
hello_dev.ko · tmp_i2c.ko
↕ sous-systèmes kernel (I2C core, GPIO...)
Hardware
registres, bus I2C physique

Character Device — Séance 20

Un character device crée une entrée dans /dev/ accessible depuis userspace comme un fichier ordinaire. Le kernel route les appels open/read/write vers nos fonctions via la struct file_operations.

C · hello_dev.c — structure complète
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ElieSas");
MODULE_DESCRIPTION("Character device minimal");

/* Variables du module — static = privées au fichier .c */
static int    major_number;
static char   message[256] = {0};
static size_t message_len;

/* ── Implémentation des callbacks file_operations ── */

static int dev_open(struct inode *i, struct file *f) {
    printk(KERN_INFO "hello_dev: opened\n");
    return 0;
}

static int dev_release(struct inode *i, struct file *f) {
    printk(KERN_INFO "hello_dev: closed\n");
    return 0;
}

static ssize_t dev_read(struct file *f, char __user *buf,
                          size_t len, loff_t *off) {
    /* copy_to_user : kernel → userspace (ne jamais utiliser memcpy !) */
    if (copy_to_user(buf, message, message_len)) return -EFAULT;
    return message_len;
}

static ssize_t dev_write(struct file *f, const char __user *buf,
                           size_t len, loff_t *off) {
    /* copy_from_user : userspace → kernel */
    if (copy_from_user(message, buf, len)) return -EFAULT;
    message_len = len;
    return len;
}

/* ── Table des callbacks — lien entre VFS et nos fonctions ── */
static struct file_operations fops = {
    .owner   = THIS_MODULE,
    .open    = dev_open,
    .release = dev_release,
    .read    = dev_read,
    .write   = dev_write,
};

/* ── Cycle de vie du module ── */
static int __init hello_init(void) {
    major_number = register_chrdev(0, "hello_dev", &fops);
    printk(KERN_INFO "hello_dev: major=%d\n", major_number);
    return 0;
}

static void __exit hello_exit(void) {
    unregister_chrdev(major_number, "hello_dev");
}

module_init(hello_init);
module_exit(hello_exit);

Commandes essentielles

// Workflow module kernel
CommandeAction
makeCompiler le module → hello_dev.ko
sudo insmod hello_dev.koCharger le module dans le kernel
lsmod | grep helloVérifier que le module est chargé
cat /proc/devicesTrouver le numéro major attribué
sudo mknod /dev/hello c 240 0Créer le fichier device (major=240, minor=0)
dmesg | tail -10Lire les printk() du module
echo "test" > /dev/helloÉcrire depuis userspace → dev_write()
cat /dev/helloLire depuis userspace → dev_read()
sudo rmmod hello_devDécharger le module

Driver I2C kernel — Séance 21

Le driver I2C s'intègre dans le sous-système I2C du kernel via i2c_driver et les callbacks probe/remove. Le kernel appelle probe automatiquement quand un périphérique correspondant est détecté sur le bus.

// Architecture driver I2C vs character device
Character device (séance 20)
register_chrdev()
→ /dev/hello
Interface : open/read/write
Pas d'intégration sous-système
Driver I2C (séance 21)
i2c_add_driver()
→ /sys/bus/i2c/devices/
Interface : sysfs automatique
Intégration complète kernel
C · tmp_i2c.c — driver I2C capteur température
#include <linux/i2c.h>
#include <linux/module.h>
#include <linux/sysfs.h>
#include <linux/device.h>
#include <linux/slab.h>

/* Structure privée — une instance par capteur détecté */
struct tmp_data {
    struct i2c_client *client;  // handle vers le périphérique I2C
    int                temp_raw;
};

/* Attribut sysfs — expose /sys/.../temperature en lecture */
static ssize_t temperature_show(struct device *dev,
                                  struct device_attribute *attr, char *buf) {
    struct tmp_data *data = dev_get_drvdata(dev);
    /* Sur vrai hardware : i2c_smbus_read_word_data(data->client, 0x00) */
    data->temp_raw += 1;  // simulation
    return sprintf(buf, "%d\n", data->temp_raw);
}
static DEVICE_ATTR_RO(temperature);  // macro kernel — génère l'attribut

/* probe() — appelé par le kernel quand le périphérique est détecté */
static int tmp_probe(struct i2c_client *client) {
    struct tmp_data *data = devm_kzalloc(&client->dev,
                             sizeof(*data), GFP_KERNEL);
    if (!data) return -ENOMEM;

    data->client = client;
    dev_set_drvdata(&client->dev, data);  // stocker le contexte

    device_create_file(&client->dev, &dev_attr_temperature);
    dev_info(&client->dev, "tmp_i2c: probe ok @ 0x%02x\n", client->addr);
    return 0;
}

/* remove() — appelé lors du rmmod ou déconnexion */
static void tmp_remove(struct i2c_client *client) {
    device_remove_file(&client->dev, &dev_attr_temperature);
    dev_info(&client->dev, "tmp_i2c: removed\n");
}

/* Table de correspondance — déclenche le probe pour adresse 0x48 */
static const struct i2c_device_id tmp_id[] = {
    { "tmp102", 0 }, {}
};
MODULE_DEVICE_TABLE(i2c, tmp_id);

static struct i2c_driver tmp_driver = {
    .driver = { .name = "tmp102", .owner = THIS_MODULE },
    .probe  = tmp_probe,
    .remove = tmp_remove,
    .id_table = tmp_id,
};
module_i2c_driver(tmp_driver);  // macro qui gère init/exit automatiquement

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ElieSas");
// devm_ = device-managed memory

Toujours préférer devm_kzalloc à kmalloc — la mémoire est libérée automatiquement par le kernel lors du remove. Évite les fuites mémoire en cas d'erreur dans probe.

⚠ Règles kernel space

En espace kernel : pas de libc, pas de float, pas de printf (→ printk). Pour les accès userspace : toujours copy_to_user() / copy_from_user(). Un crash kernel = kernel panic — pas de try/catch.

Points clés à retenir

file_operations = contrat VFS
La struct file_operations est le lien entre le VFS et notre driver. Chaque champ est un pointeur de fonction. Les champs non remplis restent NULL → opération non supportée.
Numéro majeur = type de device
Le major identifie le driver. Le minor identifie l'instance. Passer 0 à register_chrdev → le kernel attribue un major dynamiquement.
probe/remove = cycle de vie
Le kernel appelle probe quand un périphérique matche la table d'ID. remove est appelé au déchargement ou déconnexion. Toujours nettoyer dans remove ce qu'on a créé dans probe.
sysfs > /dev pour les capteurs
Pour des capteurs lus périodiquement, l'interface sysfs (/sys/.../temperature) est plus propre qu'un character device — intégration avec udev, Python, et les outils Linux standard.