Control de LEDs en Linux mediante el Subsistema Pinctrl y Árbol de Dispositivos

Configuración del Árbol de Dispositivos

Para gestionar los pines GPIO a través del subsistema pinctrl, es necesario definir las configuraciones de los pines dentro del nodo controlador. Añadiremos dos estados diferentes para controlar los LEDs conectados a los pines GPB5 a GPB8: uno para encenderlos todos y otro para apagarlos.

Modificación del archivo pinctrl (s3c2440-pinctrl.dtsi)

En la ruta arch/arm/boot/dts/s3c2440-pinctrl.dtsi, agregamos los siguientes nodos secundarios al controlador de pines:

leds_activos: estado-leds-encendidos {
    samsung,pins = "gpb-5","gpb-6","gpb-7","gpb-8";
    samsung,pin-function = <EXYNOS_PIN_FUNC_OUTPUT>;
    samsung,pin-val = <0x0>;         /* Nivel bajo para encender los LEDs */
    samsung,pin-pud = <EXYNOS_PIN_PULL_UP>;
};

leds_inactivos: estado-leds-apagados {
    samsung,pins = "gpb-5","gpb-6","gpb-7","gpb-8";
    samsung,pin-function = <EXYNOS_PIN_FUNC_OUTPUT>;
    samsung,pin-val = <0x1>;         /* Nivel alto para apagar los LEDs */
    samsung,pin-pud = <EXYNOS_PIN_PULL_UP>;
};

El nodo leds_activos configura los pines en modo salida con valor bajo, encendiendo los LEDs. Por el contrario, leds_inactivos establece el valor alto de salida para apagarlos.

Modificación del archivo de la placa (s3c2440-smdk2440.dts)

En la ruta arch/arm/boot/dts/s3c2440-smdk2440.dts, definimos el nodo del dispositivo LED especificando sus estados de pinctrl:

nodo_mis_leds: mis_leds {
    compatible = "mis_leds_compat";
    status = "okay";
    pinctrl-names = "default", "apagado";
    pinctrl-0 = <&leds_activos>;
    pinctrl-1 = <&leds_inactivos>;
};

Se definen dos estados para el dispositivo: default (que referencia la configuración leds_activos) y apagado (que referencia leds_inactivos).

Desarrollo del Controlador de Carácter

Estructura del dispositivo y operaciones de archivo

#define CANTIDAD_DISPS 1
#define NOMBRE_DRV "mis_leds_compat"

/* Estructuras internas para depuración de pinctrl (no expuestas por defecto) */
struct config_mux_local {
    unsigned grupo;
    unsigned funcion;
};
struct config_pines_local {
    unsigned pin_o_grupo;
    unsigned long *configuraciones;
    unsigned num_configuraciones;
};
struct ajuste_pinctrl_local {
    struct list_head nodo;
    enum pinctrl_map_type tipo;
    struct pinctrl_dev *pctl_dev;
    const char *nombre_dev;
    union {
        struct config_mux_local mux;
        struct config_pines_local configs;
    } datos;
};
struct estado_pinctrl_local {
    struct list_head nodo;
    const char *nombre;
    struct list_head ajustes;
};

struct datos_led {
    dev_t id_dev;
    struct cdev char_dev;
    struct class *clase_dev;
    struct device *disp_dev;
    struct device_node *nd_dev;
    struct pinctrl *pctl;
    struct pinctrl_state *estado_pctl;
};

static struct datos_led dispositivo_led;

static int led_abrir(struct inode *inode, struct file *file)
{
    return 0;
}

static ssize_t led_escribir(struct file *file, const char __user *buf, size_t cnt, loff_t *off)
{
    int dato_usuario, resultado;
    unsigned long bytes_no_copiados = copy_from_user(&dato_usuario, buf, cnt);
    if (bytes_no_copiados) return -EFAULT;

    if (dato_usuario == 1) {
        dispositivo_led.estado_pctl = pinctrl_lookup_state(dispositivo_led.pctl, "default");
        if (IS_ERR(dispositivo_led.estado_pctl)) {
            return PTR_ERR(dispositivo_led.estado_pctl);
        }
        resultado = pinctrl_select_state(dispositivo_led.pctl, dispositivo_led.estado_pctl);
        if (resultado < 0) return resultado;
    } else {
        dispositivo_led.estado_pctl = pinctrl_lookup_state(dispositivo_led.pctl, "apagado");
        if (IS_ERR(dispositivo_led.estado_pctl)) {
            return PTR_ERR(dispositivo_led.estado_pctl);
        }
        resultado = pinctrl_select_state(dispositivo_led.pctl, dispositivo_led.estado_pctl);
        if (resultado < 0) return resultado;
    }
    return cnt;
}

static struct file_operations fops_led = {
    .owner = THIS_MODULE,
    .open = led_abrir,
    .write = led_escribir,
};

Función Probe del Platform Driver

Durante la inicialización, la función probe obtiene el manejador pinctrl asociado al dispositivo, aplica el estado por defecto y registra el dispositivo de carácter. Cabe destacar que el núcleo ya aplica el estado default automáticamente durante el emparejamiento a través de pinctrl_bind_pins, pero aquí realizamos la búsqueda explícita para depurar la configuración de los pines.

static int led_probe(struct platform_device *pdev)
{
    int res = 0, i = 0;
    struct ajuste_pinctrl_local *ajuste_actual;
    struct dev_pin_info *info_pines;
    
    info_pines = pdev->dev.pins;
    dispositivo_led.pctl = info_pines->p;
    if (IS_ERR(dispositivo_led.pctl)) return PTR_ERR(dispositivo_led.pctl);

    dispositivo_led.estado_pctl = pinctrl_lookup_state(dispositivo_led.pctl, "default");
    if (IS_ERR(dispositivo_led.estado_pctl)) return PTR_ERR(dispositivo_led.estado_pctl);

    res = pinctrl_select_state(dispositivo_led.pctl, dispositivo_led.estado_pctl);
    if (res < 0) return res;

    list_for_each_entry(ajuste_actual, &(dispositivo_led.estado_pctl->settings), node) {
        if (ajuste_actual->tipo == PIN_MAP_TYPE_MUX_GROUP) {
            pr_info("Ajuste MUX: Grupo %d, Func %d\n", ajuste_actual->datos.mux.grupo, ajuste_actual->datos.mux.funcion);
        } else {
            for(i=0; i<ajuste_actual->datos.configs.num_configuraciones; i++){
                pr_info("Ajuste Config: Pin/Grupo %d, Val %lu\n", ajuste_actual->datos.configs.pin_o_grupo, ajuste_actual->datos.configs.configuraciones[i]);
            }
        }
    }

    res = alloc_chrdev_region(&dispositivo_led.id_dev, 0, CANTIDAD_DISPS, NOMBRE_DRV);
    if (res < 0) goto error_id_dev;

    cdev_init(&dispositivo_led.char_dev, &fops_led);
    res = cdev_add(&dispositivo_led.char_dev, dispositivo_led.id_dev, CANTIDAD_DISPS);
    if (res < 0) goto error_cdev;

    dispositivo_led.clase_dev = class_create(THIS_MODULE, NOMBRE_DRV);
    if (IS_ERR(dispositivo_led.clase_dev)) {
        res = PTR_ERR(dispositivo_led.clase_dev);
        goto error_clase;
    }

    dispositivo_led.disp_dev = device_create(dispositivo_led.clase_dev, NULL, dispositivo_led.id_dev, NULL, NOMBRE_DRV);
    if (IS_ERR(dispositivo_led.disp_dev)) {
        res = PTR_ERR(dispositivo_led.disp_dev);
        goto error_device;
    }
    return 0;

error_device:
    class_destroy(dispositivo_led.clase_dev);
error_clase:
    cdev_del(&dispositivo_led.char_dev);
error_cdev:
    unregister_chrdev_region(dispositivo_led.id_dev, CANTIDAD_DISPS);
error_id_dev:
    return res;
}
</ajuste_actual->

Función Remove y definición del Driver

static int led_remove(struct platform_device *pdev)
{
    device_destroy(dispositivo_led.clase_dev, dispositivo_led.id_dev);
    class_destroy(dispositivo_led.clase_dev);
    cdev_del(&dispositivo_led.char_dev);
    unregister_chrdev_region(dispositivo_led.id_dev, CANTIDAD_DISPS);
    return 0;
}

static const struct of_device_id tabla_compat_led[] = {
    { .compatible = NOMBRE_DRV, },
    { },
};

static struct platform_driver drv_led = {
    .probe = led_probe,
    .remove = led_remove,
    .driver = {
        .name = NOMBRE_DRV,
        .of_match_table = tabla_compat_led,
    }
};

module_platform_driver(drv_led);
MODULE_LICENSE("GPL");

Aplicación de Prueba en Espacio de Usuario

#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char **argv)
{
    int fd, comando;
    if (argc != 3) {
        printf("Uso: %s /dev/mis_leds_compat encender|apagar\n", argv[0]);
        return 0;
    }

    fd = open(argv[1], O_RDWR);
    if (fd == -1) {
        printf("Error al abrir %s\n", argv[1]);
        return 0;
    }

    if (!strcmp("encender", argv[2])) {
        comando = 1;
    } else if (!strcmp("apagar", argv[2])) {
        comando = 0;
    } else {
        close(fd);
        return 0;
    }

    write(fd, &comando, sizeof(comando));
    close(fd);
    return 0;
}

Despliegue y Pruebas

Compilación del Árbol de Dispositivos y el Módulo

Compilar el DTB y copiarlo al servidor TFTP:

make dtbs
cp arch/arm/boot/dts/s3c2440-smdk2440.dtb /srv/tftp/

Compilar el módulo del kernel y la aplicación de usuario, trasladándolos al sistema de archivos raíz NFS:

make -C /ruta/al/kernel/linux-5.2.8 M=$(pwd) modules
cp led_pinctrl_drv.ko /srv/nfs/rootfs/
arm-linux-gcc -o test_led test_led.c
cp test_led /srv/nfs/rootfs/

Arranque y Verificación

Tras cargar el kernel y el DTB en memoria medainte U-Boot, iniciar el sistema. Una vez en la consola Linux, insertar el módulo:

insmod led_pinctrl_drv.ko

Verificar la creación automática del nodo de dispositivo:

ls -l /dev/mis_leds_compat

Ejecutar la aplicación de prueba para observar el cambio de estados físicos en los LEDs:

./test_led /dev/mis_leds_compat encender
./test_led /dev/mis_leds_compat apagar

Finalmente, descargar el módulo del kernel cuando ya no se requiera:

rmmod led_pinctrl_drv

Etiquetas: linux-device-tree pinctrl linux-char-driver S3C2440 embedded-linux

Publicado el 9-13 21:04