Linux Device Drivers – Parte 2
- por Sergio Prado
Na parte 1 desta série de artigos, dei uma introdução sobre o desenvolvimento de drivers de dispositivos em Linux. Vimos detalhes de um sistema com Linux embarcado, a arquitetura do kernel e como as aplicações se comunicam com o hardware.
Neste artigo vamos finalmente colocar a mão na massa e codificar um driver de dispositivo de caractere (char driver). Desenvolveremos o “Hello world!” do software embarcado: acenderemos leds!
Usaremos o kit mini2440, que possui 4 leds disponibilizados nas portas de I/O GPB5 à GPB8. Criaremos 4 arquivos de dispositivo (/dev/led1, /dev/led2, /dev/led3, /dev/led4) para gerenciar o status destes leds. Enviar “1” para “/dev/led1” irá acender o led 1, e enviar “0” irá apagá-lo.
ESQUELETO DE UM DEVICE DRIVER
Muito bem, chega de teorias e bla bla bla. Este é o esqueleto de um device driver:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
#include "linux/fs.h" #include "linux/cdev.h" #include "linux/module.h" #include "linux/kernel.h" #include "linux/device.h" #include "asm/uaccess.h" #include "asm/io.h" #include "linux/slab.h" #define DEVICE_NAME "leds" #define NUM_LEDS 4 #define LED_ON 1 #define LED_OFF 0 /* per led structure */ struct leds_device { int number; int status; struct cdev cdev; } leds_dev[NUM_LEDS]; /* file operations structure */ static struct file_operations leds_fops = { .owner = THIS_MODULE, .open = leds_open, .release = leds_release, .read = leds_read, .write = leds_write, }; /* leds driver major number */ static dev_t leds_dev_number; /* driver initialization */ int __init leds_init(void) { int ret, i; /* request device major number */ if ((ret = alloc_chrdev_region(&leds_dev_number, 0, NUM_LEDS, DEVICE_NAME) < 0)) { printk(KERN_DEBUG "Error registering device!\n"); return ret; } /* init leds GPIO port */ initLedPort(); /* init each led device */ for (i = 0; i < NUM_LEDS; i++) { /* init led status */ leds_dev[i].number = i + 1; leds_dev[i].status = LED_OFF; /* connect file operations to this device */ cdev_init(&leds_dev[i].cdev, &leds_fops); leds_dev[i].cdev.owner = THIS_MODULE; /* connect major/minor numbers */ if ((ret = cdev_add(&leds_dev[i].cdev, (leds_dev_number + i), 1))) { printk(KERN_DEBUG "Error adding device!\n"); return ret; } /* init led status */ changeLedStatus(leds_dev[i].number, LED_OFF); } printk("Leds driver initialized.\n"); return 0; } /* driver exit */ void __exit leds_exit(void) { int i; /* delete devices */ for (i = 0; i < NUM_LEDS; i++) { cdev_del(&leds_dev[i].cdev); } /* release major number */ unregister_chrdev_region(leds_dev_number, NUM_LEDS); printk("Exiting leds driver.\n"); } module_init(leds_init); module_exit(leds_exit); MODULE_LICENSE("GPL"); |
Para começar, veja que existem duas funções definidas: leds_init(), que será chamada na inicialização do driver, ao ser carregado para a memória; e leds_exit(), que por sua vez será chamada ao remover o driver.
Existem também duas estruturas definidas: leds_dev[] armazena as informações de cada um dos 4 leds, e leds_fops armazena as operações que poderão ser realizadas com os leds (os nomes são auto-explicativos). Dentro de leds_dev[], temos cdev, que é a estrutura que representa um char device.
A primeira tarefa que um driver deve executar é definir o “major number” que estará associado a ele. Ele pode requisitar um determinado número fixo com a função register_chrdev_region(), ou pedir para o kernel gerar um número dinamicamente com a função alloc_chrdev_region(). Requisitar um número fixo pode ser um risco pois não sabemos se ele já esta sendo usado, portanto a solução mais segura é requisitá-lo dinamicamente (é o que fazemos na linha 42).
Na chamada a alloc_chrdev_region(), requisitamos ao kernel a alocação de 4 “minor numbers” (parâmetro 3), começando por “0” (parâmetro 2) e armazenando o “major number” em leds_dev_number (parâmetro 1). O quarto parâmetro diz ao kernel o nome associado ao driver, que você poderá ver quando listar o dispositivo em “/proc/devices“.
Na linha 51 temos um loop a ser executado para cada device (led) que criaremos. Na linha 58, a função cdev_init() associa a estrutura do led “cdev” (parâmetro 1) às funções de acesso ao arquivo de dispositivo (parâmetro 2).
Na linha 62 é que a mágica acontece. A função cdev_add() associa a estrutura do led cdev (parâmetro 1) ao seu “major e minor number” (parametro 2).
Na prática: quando você realiza uma operação de escrita em /dev/led1 (major=253 e minor=0), o kernel procura a estrutura cdev associada a este device (mesmo major/minor numbers), que você definiu em cdev_add(), e então passa esta estrutura como parâmetro para a função write(), que você associou na chamada a cdev_init(). Simples, não?
Mas como é então a implementação das funções de operação nos arquivos de dispositivo?
ACESSANDO O HARDWARE PELO ARQUIVO DE DISPOSITIVO
Não tem segredo, o conceito é simples:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
/* open led file */ int leds_open(struct inode *inode, struct file *file) { struct leds_device *leds_devp; /* get cdev struct */ leds_devp = container_of(inode->i_cdev, struct leds_device, cdev); /* save cdev pointer */ file->private_data = leds_devp; /* return OK */ return 0; } /* close led file */ int leds_release(struct inode *inode, struct file *file) { /* return OK */ return 0; } /* read led status */ ssize_t leds_read(struct file *file, char *buf, size_t count, loff_t *ppos) { struct leds_device *leds_devp = file->private_data; if (leds_devp->status == LED_ON) { if (copy_to_user(buf, "1", 1)) return -EIO; } else { if (copy_to_user(buf, "0", 1)) return -EIO; } return 1; } ssize_t leds_write(struct file *file, const char *buf, size_t count, loff_t *ppos) { struct leds_device *leds_devp = file->private_data; char kbuf = 0; if (copy_from_user(&kbuf, buf, 1)) { return -EFAULT; } if (kbuf == '1') { changeLedStatus(leds_devp->number, LED_ON); leds_devp->status = LED_ON; } else if (kbuf == '0') { changeLedStatus(leds_devp->number, LED_OFF); leds_devp->status = LED_OFF; } return count; } |
A função leds_open() é chamada na abertura do arquivo. Sua única responsabilidade, no nosso caso, é salvar a estrutura leds_dev associada ao device (led), pois usaremos esta estrutura nas funções de leitura e escrita. Isso é feito através da função container_of() na linha 7. O ponteiro para leds_dev é salvo em file->private_data na linha 10.
Na função de leitura leds_read(), recuperamos a estrutura do led leds_dev em file->private_data na linha 26, verificamos o staus do led e retornamos “1” para aceso e “0” para apagado. Veja que, para retornar dados para a aplicação, você precisa usar a função copy_to_user(). Esta é uma forma do kernel enviar dados para a aplicação, já que o kernel não “enxerga” a memória dos processos rodando em “user mode“, assim como a aplicações não enxergam a memória dos processos rodando em “kernel mode“.
Na função de escrita leds_write(), também recuperamos a estrutura do led leds_dev em file->private_data na linha 42. E usamos também a função copy_from_user() na linha 45 para ler os dados recebidos da aplicação. E então, dependendo dos dados recebidos (“0” ou “1“), mudamos o estado dos leds.
Bom, agora falta o principal: as rotinas de acesso às portas de I/O para gerenciar os leds da mini2440.
ESCOVANDO UM POUCO DE BITS
Iremos acessar os leds disponíveis na porta GPB da CPU, conforme configuração abaixo:
Precisamos configurar as portas como saída no registrador GPBCON, e ligar/desligar os leds no registrador GPBDAT. Os leds são acionados em nível baixo.
Esta CPU (S3C2440) mapeia o acesso ao GPB através do endereço de memória 0x56000010. Porém, este é um endereço real, e no kernel você trabalhará sempre com endereços virtuais. Olhando os fontes do kernel, verifiquei que o endereço virtual associado à este endereço real é o 0xFB000010. As definições estão no arquivo “arch/arm/mach-s3c2410/include/mach/regs-gpio.h“.
Vamos então dar uma olhada no código:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#define GPB_BASE 0xFB000010 #define GPBCON GPB_BASE #define GPBDAT GPB_BASE + 4 #define CLEAR_PORT_MASK 0xFFFC03FF #define SET_WRITE_PORT_MASK 0x00015400 /* initialize led port - GPB */ void initLedPort(void) { void __iomem *base = (void __iomem *)GPBCON; u32 port = __raw_readl(base); port &= CLEAR_PORT_MASK; port |= SET_WRITE_PORT_MASK; __raw_writel(port, base); } /* change led status */ void changeLedStatus(int led_num, int status) { void __iomem *base = (void __iomem *)GPBDAT; u32 mask, data; data = __raw_readl(base); mask = 0x01 << (4 + led_num); switch (status) { case LED_ON: mask = ~mask; data &= mask; break; case LED_OFF: data |= mask; break; } __raw_writel(data, base); } |
A função initLedPort() é chamada na carga do driver e irá inicializar as portas dos leds (GPB5-GPB8) para trabalhar como saída através do registrado GPBCON (bits 10..17). Na linha 15 zeramos a configuração e na linha 16 configuramos as portas como saída.
Veja que estamos usando as macros __raw_readl() para ler um inteiro de 4 bytes da memória virtual e __raw_writel() para escrever um inteiro de 4 bytes na memória virtual. Esse mecanismo é mais seguro para acessar I/O mapeado em memória em Linux.
A função changeLedStatus() irá mudar o status do led para aceso ou apagado através do registrador GPBDAT (bits 5..8).
COMPILANDO E TESTANDO
Para compilar, basta seguir os procedimentos que descrevi neste artigo.
Para transferir para a mini2440, você pode colocar a placa em rede com seu PC, e usar alguma aplicação de transferência de arquivos como o scp ou o ftp. Você pode também configurar um sistema de arquivos com NFS, conforme descrevi aqui.
Alguns dos comandos que usaremos para testes assumem que o módulo compilado (leds.ko) foi copiado para a mini2440 no diretório /lib/modules/2.6.32.2-FriendlyARM/kernel/drivers/char/.
Portanto, adapte os comandos se necessário.
O primeiro passo é iniciar o driver. Uma mensagem será exibida indicando sucesso na inicialização e todos os leds serão apagados.
$ insmod /lib/modules/2.6.32.2-FriendlyARM/kernel/drivers/char/leds.ko Leds driver initialized. |
Agora você precisa verificar o “major number” alocado pelo kernel para o device driver usando o comando abaixo (veja que, no meu caso, foi alocado o número 253):
$ grep leds /proc/devices 253 leds |
Com o “major number” em mãos, criaremos todos os arquivos de dispositivo com os comandos abaixo (mude o valor do major number se necessário):
$ mknod /dev/led1 c 253 0 $ mknod /dev/led2 c 253 1 $ mknod /dev/led3 c 253 2 $ mknod /dev/led4 c 253 3 $ chmod a+w /dev/led* |
Obs: No próximo artigo veremos como usar um mecanismo de hotplug para criação dinâmica de arquivos de dispositivo.
Agora é hora de testar. Para acender um led, basta enviar o caracter “1”. Os comandos abaixo acendem os leds 1 e 3:
$ echo "1" > /dev/led1 $ echo "1" > /dev/led3 |
E para apagar um led, basta enviar “0”:
$ echo "0" > /dev/led3 |
Para verificar o estado de um led, não podemos apenas ler o arquivo com o comando “cat” por exemplo. Isso porque na rotina que trata a leitura do arquivo, perceba que nunca retornamos EOF (end-of-file). Isso significa que, se por exemplo o led estiver aceso, o comando “cat /dev/led1” irá imprimir indefinidamente o número “1“. Então, para ler o estado do led, precisamos de um comando que leia apenas 1 byte. Podemos fazer isso com o comando “dd“, salvar o byte em um arquivo, e depois imprimir, conforme abaixo:
$ dd if=/dev/led1 of=/tmp/led bs=1 count=1 $ echo "LED=$(cat /tmp/led)" LED=1 |
E finalmente, para remover o driver:
$ rmmod leds Exiting leds driver. |
PRÓXIMOS PASSOS
Muito bem, vocês viram que o assunto deste artigo foi bem denso. Se para você foi muita informação de uma só vez, pare, respire, pense. Estude o código com mais calma, e você verá que não existe nenhum segredo no desenvolvimento de device drivers para Linux. Basta conhecer a API do Linux e escovar um pouco de bits.
No próximo artigo vamos aprender alguns novos conceitos, como criação dinâmica de arquivos de dispositivo, gerenciamento do hardware através do diretório “/sys” e rotinas de timer do kernel (vamos adicionar uma funcionalidade de pisca-pisca ao nosso driver).
Um abraço!
Sergio Prado