Linux Device Drivers – Parte 2

- por Sergio Prado

Categorias: Linux, Mini2440 Tags: , ,

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

Faça um Comentário

Navegue
Creative Commons Este trabalho de Sergio Prado é licenciado pelo
Creative Commons BY-NC-SA 3.0.