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
| #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)
{
/* 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
O código-fonte completo e o Makefile podem ser baixados aqui. 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 abaixo:
/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”:
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
VN:F [1.9.17_1161]
Rating: 9.8/10 (12 votes cast)
Linux Device Drivers — Parte 2, 9.8 out of 10 based on 12 ratings Posts relacionados:
- Mini2440 — Compilando aplicações e device drivers
- Linux Device Drivers — Parte 1
- Mini2440 — Linux com U-Boot e Emdebian