XMLTagsEditHistoryDiscussion

Programación de drivers para dispositivos

  1. Programación de drivers para dispositivos
    1. ¿Qué es un driver?
    2. ¿Por qué escribir un driver?
    3. Recomendaciones
      1. Proveer mecanismos, y no políticas
    4. Uso de módulos
      1. Módulos de ejemplo
      2. Hola Mundo
      3. Ejercicio 1
      4. Módulo simple con un driver de caracter
        1. Sintaxis de inicialización "C Tagged Structures"
      5. Módulo para un driver de caracter
      6. Módulo para un driver de con lectura bloqueante y no bloqueante
      7. Módulo para capturar una interrupción
        1. Entradas en el /proc
        2. Restricciones de un manejador de interrupción:
        3. Work queues
    5. Referencias

Mantenido por Nelson Castillo.

La idea de este documento es documentar el desarrollo de un driver simple para Linux. El código que se muestra en esta página ha sido probado en las arquitecturas ARM, Intel y User-Mode Linux.

¿Qué es un driver?

Un driver es una capa de código entre el dispositivo de hardware y la aplicación. Un driver usa los privilegios con los que se ejecuta su código para definir exactamente como se quiere que un dispositivo sea visto por una aplicación. Pueden existir diferentes drivers para un mismo dispositivo.

El driver está en el kernel. El kernel tiene las siguientes tareas:

El kernel diferencia tres tipos de drivers:

No todos los drivers son de dispositivos. Algunos son de software. Por ejemplo, el driver de un sistema de archivos como ext3 o reiserfs son drivers de software, que mapea estructuras de datos de bajo nivel a estructuras de datos de más alto nivel.

¿Por qué escribir un driver?

Existen muchas razones para querer escribir un driver.

Recomendaciones

Proveer mecanismos, y no políticas

La idea es que un driver permita acceder a los dispositivos de hardware, sin imponer restricciones arbitrarias a los que usan el driver. En UNIX esto es una regla de diseño, que se conoce como separación de mecanismos y políticas.

Para no escribir políticas en el driver, se puede hacer una aplicación de usuario que se encargue de configurar el dispositivo, y/o una librería para acceder a él. La idea es que el driver cambie muy poco.

Uso de módulos

Los módulos tienen la ventaja de permitir adicionar y remover funcionalidades del kernel mientras el sistema está corriendo.

Módulos de ejemplo

Para obtener los módulos de ejemplo, ejecute:

 $ svn co http://svn.arhuaco.org/svn/src/linux/examples/modules/hello/
 $ cd hello

Hola Mundo

El siguiente ejemplo hello1.c es el código de un módulo que puede ser cargado y descargado de memoria con insmod y rmmod.

#include <linux/init.h>
#include <linux/module.h>

MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
        printk(KERN_ALERT "Hola, mundo\n");
        return 0;
}

static void hello_exit(void)
{
        printk(KERN_ALERT "Adios, mundo cruel\n");
}

module_init(hello_init);
module_exit(hello_exit);

La macro MODULE_LICENSE especifica la licencia del módulo. Si no se especifica esta cadena, se produce un warning, por ejemplo:

hello1: module license 'unspecified' taints kernel.

Se recomienda especificar la licencia, a no ser que se esté creando un módulo propietario.

Las macros module_init y module_exit son para inicialización. Funcionan si código se compila como módulo y directamente en el kernel. Cuando el código se compila en el kernel module_init se comporta igual a __initcall().

Para compilar el módulo, se usa el siguiente Makefile:

obj-m += hello1.o

all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

La compilación de los módulos se ha simplificado, antes era necesario especificar múltiples parámetros en un makefile para un módulo. El nuevo sistema para compilar el kernel facilita el proceso de compilación de módulos.

Para compilar un módulo para User-mode Linux, se escribiría solamente.

uml@uml:~/hello$ ARCH=um make
make -C /lib/modules/2.6.16.18/build M=/home/uml/hello modules
make[1]: Entering directory `/home/n/uml/linux-2.6.16.18'
  CC [M]  /home/uml/hello/hello1.o
  Building modules, stage 2.
  MODPOST
  CC      /home/uml/hello/hello1.mod.o
  LD [M]  /home/uml/hello/hello1.ko
make[1]: Leaving directory `/home/n/uml/linux-2.6.16.18'

Ahora podemos insertar el módulo en el kernel.

# insmod hello1.ko
Hola, mundo

Al borrar el módulo, se llama la función especificada en la macro module_exit. En este caso, es la función hello_exit.

# rmmod hello1

Adios, mundo cruel

Ejercicio 1

En esta página hay código de ejemplo para la creación de una entrada en el filesystem /proc. La función remove_proc_entry borra una entrada de este filesystem.

 
 remove_proc_entry(PROC_NAME, NULL);

Escribir un módulo que provea la entrada /proc/count, con un contador. (solución)

Módulo simple con un driver de caracter

Los drivers en el kernel son accedidos por medio de un archivo especial. Estos archivos se pueden crear con el comando mknod.

mknod [OPTION]... NAME TYPE [MAJOR MINOR]
# mknod prueba char 1 1
# ls -l prueba
crw-r--r-- 1 root root 1, 1 2006-05-31 05:55 prueba
# rm prueba

En el archivo Documentation/devices.txt se encuentra un listado de los dispositivos que ya tienen un nombre asignado en Linux.

Al hacer un driver existen dos opciones: tomar un número mayor y menor no asignado, o usar uno dinámico. Cuando se usa asignación dinámica, se puede leer los números asignados de /proc/devices.

$ cat /proc/devices
Character devices:
  1 mem
  2 pty
  3 ttyp
  4 /dev/vc/0
  4 tty
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  7 vcs
 10 misc
 13 input
 14 sound
 29 fb
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
216 rfcomm
226 drm

$ ls -l /dev/console
crw------- 1 root root 5, 1 2006-05-31 04:54 /dev/console
$ ls -l /dev/input/mice
crw-rw---- 1 root root 13, 63 2006-05-31 12:52 /dev/input/mice
Sintaxis de inicialización "C Tagged Structures"

En el código del kernel es normal encontrar incializaciones de estructuras que no asignan un valor a todos los campos de la estructura. Estas inicializaciones se hacen usando la siguiente sintaxis:

#include <stdio.h>

struct prueba
{
 int a;
 int b;
};

int main(int argc, char *argv[])
{

  struct prueba x =
  {
    .a = 1,
  };

  printf("x.a = %d\n", x.a);

  return 0;
}

Las inicializaciones de este estilo permiten inicializar los miembros de la estructura sin seguir un orden en particular, y sin que sea necesario inicializar todos los campos.

El siguiente es el código de ejemplo de un driver de caracter de lecto-escritura (código)

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <asm/uaccess.h> /* get_user and put_user */

#define SUCCESS 0
#define DEVICE_FILE_NAME "char_dev"
#define DEVICE_NAME "char_dev"
#define MAJOR_NUM 100
#define BUF_LEN 80

MODULE_LICENSE("Dual BSD/GPL");


static atomic_t Device_Open = ATOMIC_INIT(1);
static char Message[BUF_LEN]; /* Buffer */
static char *Message_Ptr;     /* ¿Qué tan lejos llegó la lectura? */

static int device_open(struct inode *inode, struct file *file)
{

#ifdef DEBUG
  printk(KERN_INFO "device_open(%p)\n", file);
#endif

  if (!atomic_dec_and_test (&Device_Open))
  {
    atomic_inc(&Device_Open);
    return -EBUSY; /* ya está abierto */
  }

  Message_Ptr = Message;
  try_module_get(THIS_MODULE);

  return SUCCESS;
}

static int device_release(struct inode *inode, struct file *file)
{
#ifdef DEBUG
  printk(KERN_INFO "device_release(%p,%p)\n", inode, file);
#endif


  atomic_inc(&Device_Open); /* marca el dispositivo como no-abierto */

  module_put(THIS_MODULE);
  return SUCCESS;
}

static ssize_t device_read(struct file *file,    /* ver include/linux/fs.h   */
                           char __user * buffer, /* buffer a llenar con datos */
                           size_t length,        /* longitud del buffer  */
                           loff_t * offset)
{
  int bytes_read = 0; /* bytes escritos en el buffer */

#ifdef DEBUG
  printk(KERN_INFO "device_read(%p,%p,%d)\n", file, buffer, length);
#endif

  if (*Message_Ptr == 0) /* retornar EOF */
    return 0;

  while (length && *Message_Ptr)
  {
    put_user(*(Message_Ptr++), buffer++); /* escribe al buffer del usuario */
    length--;
    bytes_read++;
  }

#ifdef DEBUG
  printk(KERN_INFO "Lei %d bytes, quedan %d\n", bytes_read, length);
#endif

  return bytes_read; /* bytes escritos al buffer */
}

static ssize_t
device_write(struct file *file,
             const char __user * buffer, size_t length, loff_t * offset)
{
  int i;

#ifdef DEBUG
  printk(KERN_INFO "device_write(%p,%p,%d)", file, buffer, length);
#endif

  for (i = 0; i < length && i < BUF_LEN; i++)
    get_user(Message[i], buffer + i);

  Message_Ptr = Message;

  return i;
}

struct file_operations Fops = {
  .read = device_read,
  .write = device_write,
  .open = device_open,
  .release = device_release,
};

int init_module()
{
  int ret_val;

  ret_val = register_chrdev(MAJOR_NUM, DEVICE_NAME, &Fops);

  if (ret_val < 0)
  {
    printk(KERN_ALERT "El registro del dispositivo falló (%d)\n", ret_val);
    return ret_val;
  }

  printk(KERN_INFO "Se registró el dispositivo. El major device number es %d.\n", MAJOR_NUM);
  printk(KERN_INFO "mknod %s c %d 0\n", DEVICE_FILE_NAME, MAJOR_NUM);

return 0;
}

void cleanup_module()
{
  int ret;

  ret = unregister_chrdev(MAJOR_NUM, DEVICE_NAME);

  if (ret < 0)
    printk(KERN_ALERT "Error: unregister_chrdev: %d\n", ret);
}
# insmod chardev.ko

# dmesg | tail
Se registró el dispositivo. El major device number es 100.
mknod char_dev c 100 0

# mknod /dev/char_dev c 100 0

# ls -lh /dev/char_dev
crw-r--r-- 1 root root 100, 0 2006-05-31 07:21 /dev/char_dev

# chmod a+w /dev/char_dev

Usando python podemos hacer pruebas. La idea es ver como van saliendo las líneas de depuración a medida que se hacen las llamadas.

$ python

>>> f = open("/dev/char_dev", "r")
>>> f.close()
>>> f = open("/dev/char_dev", "w")
>>> f.write("Hola\n")
>>> f.close()
>>> f = open("/dev/char_dev", "r")
>>> print f.readline()
Hola

>>> f.close()

Módulo para un driver de caracter

A diferencia de el módulo simple, este módulo:

Hecho:

Falta por hacer:

El código está en este repositorio. Tiene muchos comentarios. Si lee alguna parte y cree que falta un comentario, con gusto lo puedo agregar.

Módulo para un driver de con lectura bloqueante y no bloqueante

El módulo de ejemplo anterior siempre retorna algo al leer. Ahora trabajaremos haciendo que las funciones de lectura y escritura implementen la siguiente semántica estándar:

En este driver ya no aparece definida la llamada seek. Para que todas las llamadas a seek fallen, se ha llamado la función nonseekable_open en el método open, y se ha referenciado no_llseek (disponible en linux/fs.h) en la estructura file_operations del driver.

Algunas reglas para tener en cuenta:

El código está en este repositorio. Tiene muchos comentarios. Si lee alguna parte y cree que falta un comentario, con gusto lo adiciono.

Módulo para capturar una interrupción

Este módulo está en desarrollo

Comenzamos con el acceso a hardware. En muchas arquitecturas, el espacio de direcciones y el de los puertos de I/O es el mismo. No obstante, debido a que algunas arquitecturas implementan espacios de direcciones separados, Linux tiene funciones especiales para leer y escribir en puertos, incluso en las arquitecturas que no los tienen.

En la arquitectura ARM el espacio de memoria e I/O es el mismo.

Al programar es importante hacer que el compilador no optimice ciertas instrucciones, y para ello Linux provee las siguientes macros que garantizan que las lecturas o escrituras que están antes de la función se completen.

También se recomienda usar variables con la palabra clave volatile donde haga falta para prevenir optimizaciones del compilador.

Para solicitar el acceso exclusivo a una región de I/O, se usan las funciones request_region y release_region.

Ver el módulo en http://svn.arhuaco.org/svn/src/linux/examples/modules/parallel/.

Para escribir y leer de estos puertos, Linux define en /asm/io.h las siguientes funciones:

En algunas arquitecturas (S390) sólo se permiten operaciones de I/O de 8 bits.

Para un ejemplo de acceso a estos puertos en espacio de usuario, ver count.c.

Note que las funciones que usan words y longs pueden cambiar el orden de los bytes dependiendo de la plataforma.

También se pueden leer strings de puertos, y para ello están las siguientes funciones:

También hay funciones para leer strings de longs y de words, y estas no cambian el orden de los bytes.

Entradas en el /proc

Ver:

Restricciones de un manejador de interrupción:
Work queues

En interrupciones, hay que retornar lo más rápido posible. En el kernel 2.6 se ha implementado una nueva interfaz llamada work queue.

Ver:

http://www.linuxjournal.com/article/6916

Y un ejemplo en:

/arch/um/drivers/port_kern.c y en /arch/um/drivers/net_kern.c.

Referencias

Documentación del kernel:

Last update: 2007-03-31 (Rev 10985)

svnwiki $Rev: 12966 $