Introdução aos Sistemas Operacionais/Exemplo de Comunicação com Hardware em Drivers Linux: Título 2

Fonte: Wikiversidade

Dados gerais[editar | editar código-fonte]

Autoria[editar | editar código-fonte]

Everton Cerqueira - 050133

Wanderson Paim de Jesus - 060219

Licenciamento[editar | editar código-fonte]

Licença (GLP)

Objetivo e descrição[editar | editar código-fonte]

O objetivo do projeto é estudar o processo de desenvolvimento de dispositivos do Linux por meio da documentação e modificação de códigos-exemplo disponibilizado no livro "Linux Device Drivers - de Jonathan Corbet, Alessandro Rubini, and Greg Kroah-Hartman" e em sites direcionados ao assunto. O enfoque será no aprendizado da API de programação. Os resultados do estudo serão documentados como projetos de grupo na disciplina de Sistemas Operacionais da Wikiversity.
O projeto Blink visa o desenvolvimento de um driver para comunicação com o hardware, mais especificamente com os leds do teclado. E para isso organizamos o conteúdo de forma a facilitar o aprendizado de possíveis leitores. Inicialmente é feita uma introdução ao tema Linux Device Drivers, buscando mostrar os principais conceitos envolvidos no decorrer da publicação. Então iniciamos uma descrição da configuração de um ambiente favorável e da exemplificação de um módulo básico e suas principais funções. Em seguida apresentaremos o projeto Blink, um antecessor do driver Blink, que será pautado no último capítulo.

Introdução[editar | editar código-fonte]

Configurando Ambiente[editar | editar código-fonte]

Apresentação do projeto Blink[editar | editar código-fonte]

Para exemplificar o processo de criação de um módulo para o linux, primeiramente pensamos em criar um projeto funcional de comunicação com um hardware, o qual poderia futuramente ser adaptado para utilização como módulo.
O hardware escolhido foi o teclado, pois nele existem alguns leds de indicação que podem ser utilizados como teste. Os leds do teclado podem ser controlados diretamente por portas de E/S. As portas de E/S são utilizadas pela CPU para comunicar com quase todos os controladores e periféricos. Cada porta tem um endereço no qual podemos realizar operações de escrita e leitura.
Cada dispositivo dentro do computador tem uma faixa de portas de E/S alocadas que poderam ser utilizadas para comunicação. Para checar qual a faixa de portas que um certo dispositivo pode usar para comunicação, basta dar uma olhada no /proc/ioports. Analisando este arquivo, percebemos que o teclado usa a faixa de portas 0x60 – 0x6f. Para resolver nosso problema de controle dos leds do teclado, utilizaremos apenas a porta 0x60.

Comunicação[editar | editar código-fonte]

Para controlar os leds do teclado, devemos seguir o seguinte processo:

  1. Escrever o comando 0xED na porta 0x60. Esse comando deve indicar ao teclado que em seguida ele receberá um padrão de configuração dos leds.
  2. A controladora do teclado irá enviar pela porta 0x60 um comando de resposta 0xFA indicando que ele está pronto para receber a configuração.
  3. O teclado espera um valor do tipo byte na porta 0x60 com a configuração desejada para os leds, que segue o seguinte padrão:
  • 000: todos desligados
  • 001: somente SCROLL-LOCK ativo
  • 010: somente NUM-LOCK ativo
  • 100: somente CAPS-LOCK ativo
  • 111: todos ligados

Para ter permissão de interação com o hardware, ou seja, permissão para executar operações de E/S em modo usuário, devemos utilisar a função ioperm() ou a função iopl() da API <sys/io.h>, o que as difere é que enquanto iopl() adquire permissão para um conjunto de portas a ioperm() adquire somente para uma. O programa deve ser compilado usando a opção -o para forçar a expansão das funções internas.

Código comentado[editar | editar código-fonte]

#define BASEPORT 0x60 /* porta do teclado */	

	/* Garante acesso a porta do teclado */

  	if (ioperm(BASEPORT, 3, 1)) {perror("ioperm"); exit(1);}
	
	int retries = 5;
 /*número de tentativa para receber resposta*/

	int timeout = 1000;
 /* tempo aplicado após requisição inicial*/

	int state = atoi(argv[1]); /* configuração passada por parametro*/


	int conf = 000;
 /* configuração efetiva*/

	int i;

	for (i = 1; i <= 10; i++){
 /* para piscar o led */
		if ( (i%2) == 0){
 // se par apaga o led
			conf = 000;

		} else {
// se não, acende o led passado por parâmetro
			conf = state;		

		}

		outb(0xed,0x60); // Envia sinal de configuração


		usleep(timeout);
 // espera
		while (retries!=0 && inb(0x60)!=0xfa) { 
		/* espera pela resposta do teclado utilisando a leitura com inb() na porta 0x60 */

			retries--;

			usleep(timeout);
 // espera para nova tentativa
		}

		if (retries!=0) { // checa se conseguiu receber resposta
			outb(conf,0x60);
 // envia configuração
		}

		usleep(1000000);
 // tempo para dar efeito do led piscando
	}



  /* Liberando a porta */

  if (ioperm(BASEPORT, 3, 0)) {perror("ioperm"); exit(1);}

O módulo Blink[editar | editar código-fonte]

No intuito de tornar o projeto blink em um módulo, o primeiro passo é descobrir quais são as APIs fornecidas pelo kernel do linux que poderão substituir as utilizadas pelo nosso blink.c. Tais como stdio.h, sys/io.h, unistd.h e stdlib.h. Para substituir a função ioperm(), temos no kernel do linux a função request_region() e para que seja possível sua utilização, temos que importar a API <linux/ioport.h> do kernel. Sua utilização é demonstrada e explicada no código comentado do módulo blink.

Código fonte[editar | editar código-fonte]

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <asm/uaccess.h>	/* for put_user */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>
#include <linux/stat.h>
#include <linux/sched.h>
#include <linux/version.h>
#include <linux/ioport.h>
#include <linux/io.h>
#include <linux/fs.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/delay.h> // Simula o sleep() do C. ou wait();

int init_module(void); // chamado quando se da um insmod
void cleanup_module(void); //chamado quando se da um rmmod

static int device_open(struct inode *, struct file *); // método chamado quando um processo usa o device, exemplo: cat /dev/chardev

static ssize_t device_read(struct file *, char *, size_t, loff_t *); // implementa leitura
static ssize_t device_write(struct file *, const char *, size_t, loff_t *); // implementa escrita


#define DEVICE_NAME "blink"	/* o nome que irá aparecer em /proc/devices */
#define PORTA 0x60 /* teclado lp1 */

/* 
 * Variaveis globais
 */

static int Major;		/* Define o nosso Major number */
static int Device_Open = 0;	/* Dispositivo esta sendo usado?  
				 * previne multiplos acessos */
static char *msg_Ptr;

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

/*
 * Função chamada quando é feito o insmod
 */
int init_module(void)
{
        Major = register_chrdev(0, DEVICE_NAME, &fops); 
		/*Pedimos pelo Major 0 pois garantimos assim um Major único. 
		Se escolhessemos um número específico poderiamos estar escolhendo um já em uso.*/

	if (Major < 0) {
	  printk(KERN_ALERT "Falha ao tentar alocar dispositivo com major= %d\n", Major);
	  return Major;
	}

	printk(KERN_INFO "Fui alocado com o numero de major %d. Para comunicar com\n", Major);
	printk(KERN_INFO "o driver, crie um dev dessa forma:\n");
	printk(KERN_INFO "'mknod /dev/%s c %d 0'.\n", DEVICE_NAME, Major);

	return 0;
}

/*
 * Aloca as portas para comunicação com teclado.
 *
 */

static int alloc_port(){
    if( request_region( PORTA, 1, "chardev") == NULL){ // geralmente o teclado usa as portas dessa faixa 0x60-0x6f, mas vamos usar somente uma, a 0x60
        printk(KERN_ALERT "Não foi possível alocar essas portas.");
	    return 0;
    }else{
		printk(KERN_ALERT "Porta alocada.");
		return 1;
	}
}
/*
 * Libera a porta
 */

static void free_port(){
    release_region( PORTA,1);
}

/*
 * Função chamada quando removemos o modulo, rmmod;
 */
void cleanup_module(void)
{
	printk(KERN_ALERT "Obrigado por utilizar o modulo blink...");
}

/* 
 * Função chamada quando um processo chama o modulo, exemplo:
 * "cat /dev/blink"
 */
static int device_open(struct inode *inode, struct file *file)
{
	if (alloc_port() == 1){

		int retries = 5;
		int timeout = 1000;
		int state = 010; 
		int conf = 000;	// a configuração dos leds
		
		/*
		 * 000 para tudo apagado, 100 para capslock aceso, 010 para num lock aceso, 001 para scroll lock aceso e 111 para acender tudo.
		*/

		int i;
		for (i = 1; i <= 10; i++){ // vamos fazer com que pisque 5 vezes, 
			if ( (i%2) == 0){
				conf = 000;
			} else {
				conf = state;		
			}
			outb(0xed,0x60); // Diz ao teclado que queremos modificar a configuração dos leds.

			mdelay(timeout);
			while (retries!=0 && inb(0x60)!=0xfa) { // espera a resposta do teclado
				retries--;
				mdelay(timeout);
			}
			if (retries!=0) { // checa se o teclado está pronto para receber a nova configuração.
				outb(conf,0x60);
			}
			mdelay(1000000); // espera um tempo para dar efeito blink.
		}
	
		free_port();
		return 0;
	} else {
		return 1;	
	}
}

/* 
 * Função chamada quando um processo, que já abriu o modulo, tenta escrever nele.
 */
static ssize_t device_read(struct file *filp,
			   char *buffer,	
			   size_t length,	
			   loff_t * offset)
{
	/*
	 * Número de bytes atualmente escritos no buffer.
	 */
	int bytes_read = 0;

	/*
	 * Se chega no fim, retorna 0.
	 */
	if (*msg_Ptr == 0)
		return 0;

	/* 
	 * Põe os dados no buffer;
	 */
	while (length && *msg_Ptr) {

		/* 
		 * The buffer is in the user data segment, not the kernel 
		 * segment so "*" assignment won't work.  We have to use 
		 * put_user which copies data from the kernel data segment to
		 * the user data segment. 
		 */
		put_user(*(msg_Ptr++), buffer++);

		length--;
		bytes_read++;
	}

	/* 
	 * Retorna o numero de bytes colocados no buffer.
	 */
	return bytes_read;
}

/*  
 * Função chamada quando tentamos escrever no modulo, com "echo "oi" /dev/blink "
 */
static ssize_t
device_write(struct file *filp, const char *buff, size_t len, loff_t * off)
{
	printk(KERN_ALERT "Operação não suportada. Ao invés de echo, tente um cat.\n");
	return -EINVAL;
}

Procedimento para testes[editar | editar código-fonte]

Para iniciar os testes é preciso que os arquivos blink.c e o Makefile estejam na mesma pasta. Então é executado o comando make, que compilará o blink.c gerando, dentre outros, o blink.ko, modulo o qual será instalado utilizando o comando insmod.

Uma vez instalado, podemos checar sua disponibilidade e tambem o Major number atribuido, utilizando o comando cat /proc/devices, agora que já sabemos desses dados, podemos criar o driver no /dev da seguinte maneira: mknod /dev/blink c MN 0 - isso irá criar o nosso driver comunicador com o Major Number MN atribuido. A partir dai basta fazer um pedido de execução para ativar a função device_open do módulo e ver o resultado. Um exemplo seria cat /dev/blink. Para remover o módulo basta fazer um rmmod blink.ko.


Referências[editar | editar código-fonte]

http://www.cs.bham.ac.uk/~exr/teaching/lectures/systems/08_09/docs/kernelAPI/

http://www.makelinux.net/ldd3/chp-9-sect-2.shtml

http://tldp.org/HOWTO/IO-Port-Programming-9.html

http://lwn.net/Kernel/LDD3/

http://tldp.org/LDP/lkmpg/2.6/html/index.html

http://lwn.net/Articles/2.6-kernel-api/