Exemplo de Device Driver I2C para Linux Embarcado

Driver I2C

Depois de um longo tempo, iremos agora retomar a série sobre Device Drivers com um exemplo de device driver I2C para Linux Embarcado. Caso não tenha lido os artigos anteriores, sugiro que os leia para obter um conhecimento básico.

 

Em especial, leia o artigo que explica as diferenças entre device drivers e drivers de plataforma. Outro pré-requisito muito importante é o uso de ponteiros. Eles são muito usados no kernel Linux e nós veremos ponteiros por todo o código do driver.

 

Aqui será apresentado um exemplo de device driver I2C baseado em um driver já existente, mas reduzido ao máximo para torná-lo mais simples. O driver em que foi baseado o exemplo é o driver at24.c, que é usado para ler memórias I2C da família at24c como, por exemplo, a at24c512. Ele usa a abordagem de sysfs e cria entradas no diretório /sys para representar a memória como um arquivo e, assim, permitir que o usuário facilmente leia e escreva no dispositivo. O driver pode ser testado em qualquer placa com Linux embarcado que tenha os pinos I2C disponíveis.

 

Começaremos mostrando o código do device driver, seguido de seu Makefile, que é usado para compilá-lo. E será apresentado um exemplo de software para testar o device driver. Iniciemos baixando o código do device driver com o seguinte comando:

 

 

E entre na pasta do driver I2C dentro do kernel Linux com o comando:

 

 

O código do driver pode ser acessado aqui, mas é exibido abaixo para referência.

 

 

 

Visão geral do driver

 

Visão geral do driver I2C para Linux Embarcado.
Visão geral do driver I2C para Linux Embarcado.

 

 

Explicação do código do driver

 

Geralmente os códigos de device drivers para Linux Embarcado seguem uma lógica que começa de baixo para cima. Assim, normalmente encontramos as funções de inicialização e de probe()/remove() nas últimas linhas do código. Então passemos agora para a explicação das principais linhas do código do driver.

 

Nas linhas 513, 514 e 515 temos as macros de metadados já explicadas no artigo Exemplo de driver para Linux Embarcado. E também nas linhas 505 e 511 temos as funções module_init()/module_exit(), também já explicadas no referido artigo.

 

1. i2c_add_driver(): linha 503

 

Essa função é responsável por registrar um dispositivo I2C no subsistema I2C do kernel Linux, que irá se comunicar com um dispositivo I2C slave. Ela recebe como parâmetro um ponteiro para estrutura struct i2c_driver. Essa estrutura guarda informações sobre o driver no membro driver e ponteiros para as funções de callback probe() e remove() nos respectivos membros da estrutura. O membro id_table guarda a lista de dispositivos I2C suportados por esse driver.

 

Essa é a forma de dizer ao sistema que existe um driver chamado at24, que é capaz de se comunicar com os dispositivos listados em id_table e que tem como funções de callback at24_probe() e at24_remove().

 

2. i2c_del_driver(): linha 509

 

Essa função remove um dispositivo I2C no subsistema I2C do kernel. Assim como i2c_add_driver(), ela recebe como parâmetro um ponteiro para struct i2c_driver.

 

3. at24_probe(): linha 359

 

Todo device driver precisa fornecer ao menos a definição das funções de probe() e remove(). A função probe() é algo obscuro e difícil de depurar. Ela é chamada quando registramos o driver I2C usando i2c_add_driver(). Aqui está a sequência de chamadas para o caso do barramento I2C:

  • i2c_add_driver()
  • i2c_register_driver()
  • driver_register()
  • bus_add_driver()
  • driver_attach()
  • __driver_attach()
  • driver_probe_device()
  • really_probe() - aqui parece um beco sem saída, mas seguimos adiante
  • i2c_device_probe() - chamada por dev->bus->probe dentro de really_probe
  • at24_probe() - chamada por driver->probe() dentro de i2c_device_probe

 

Observe que a função i2c_device_probe() do barramento I2C, que está dentro de i2c-core.c, é definida na estrutura i2c_bus_type:

 

 

Quando a instrução driver->probe() é executada, é a função i2c_device_probe() que será chamada, uma vez que estamos usando o barramento I2C e esta função foi definida como callback de .probe, como mostra o código acima.

 

O ponteiro struct i2c_client *client, o primeiro parâmetro da função at24_probe(), provém da função i2c_new_device(), que é chamada em arquivos de board. Dentro da função at24_probe(), temos uma série de outras funções importantes.

 

3.1. at24_get_ofdata(): linhas 339, 354 e 388

 

Essa função recebe como parâmetro um ponteiro para struct i2c_client e outro ponteiro para struct at24_platform_data. Ela só executa algo na definição da linha 339, e serve para atualizar os parâmetros relacionados a modos de acesso (leitura/escrita) e o tamanho da página da memória. A sua existência, na linha 339, está condicionada ao teste de pré-processador #ifdef CONFIG_OF, o qual verifica se o kernel está usando Device Tree. Essas propriedades são lidas no Device Tree usando a função of_get_property(). As propriedades do nó são recuperadas por meio do ponteiro client->dev.of_node, atribuido na linha 343.

 

Obtenha mais informação sobre pré-processador e Device Tree.

 

3.2. i2c_check_functionality(): linha 396

 

Essa função verifica se o controlador I2C suporta determinada funcionalidade. No teste da linha 396 estamos verificando se há suporte para escrita e leitura de bytes por parte do controlador I2C. Então isso vai depender da placa que está sendo usada. Em geral, a maioria dos controladores I2C suportam leitura e escrita de bytes. Também é possivel verificar se há suporte para leitura e escrita de words ou operações em blocos. Veja a lista completa aqui.

 

3.3. devm_kzalloc(): linha 410

 

Essa função é usada para alocar memória. Porém, diferente da antiga kzalloc(), a memória é liberada automaticamente quando o driver é removido. Ela retorna um ponteiro para a área de memória alocada em caso de sucesso ou NULL em caso de falha.

 

3.4. sysfs_bin_attr_init(): linha 424

 

Inicializa uma estrutura bin_attribute alocada dinamicamente. Ela é necessária apenas quando o atributo lockdep está presente no kernel. Por sua vez, o lockdep irá imprimir uma descrição da situação e um traço da stack para o log do kernel quando ele encontra uma sequência de locking que pode potencialmente causar um deadlock. Nas linhas 425 a 430 temos a inicialização de alguns membros da estrutura bin_attribute. Entre eles temos bin.read e bin.write, que definem as funções para leitura e escrita, respectivamente, do arquivo binário criado sob o diretório /sys.

 

3.5. sysfs_create_bin_file(&client->dev.kobj, &at24->bin): linha 444

 

Cria um arquivo binário para o objeto passado como parâmetro sob o diretório /sys. Recebe um ponteiro para struct kobject e um ponteiro constante para struct bin_attribute. O nome do arquivo é indicado em bin.attr.name. Essa função é mais indicada quando um arquivo precisa transferir grandes quantidades de dados, como uma memória.

 

3.6. i2c_set_clientdata(client, at24): linha 448

 

Essa função é usada para guardar um ponteiro void *driver_data que armazena informações do driver. No nosso caso, estamos guardando as informações da estrutura at24 dentro da estrutura de client. Mais tarde, se for necessário, podemos recuperar esse ponteiro usando i2c_get_clientdata(). Também é possível recuperar esse ponteiro usando a função container_of() ou uma variante dela.

 

4. at24_remove: linha 469

 

Essa função é chamada quando o driver é removido do subsistema I2C do kernel, processo que é iniciado por i2c_del_driver(). Ela passa por várias funções, assim como probe(), e é chamada por dev->bus->remove(dev), dentro de __device_release_driver(). As funções dentro dela executam a liberação de recursos.

 

4.1. i2c_get_clientdata(): linha 474

 

Como dito anteriormente, recupera o ponteiro salvo por i2c_set_clientdata(). Ela recebe como parâmetro um ponteiro para struct i2c_client, e retorna um ponteiro para void.

 

4.2. sysfs_remove_bin_file(): 475

 

Remove o arquivo binário do objeto passado como parâmetro e também apaga a entrada no diretório /sys, criado por sysfs_create_bin_file().

 

4.3. i2c_unregister_device(): linha 478

 

Remove um dispositivo do subsistema I2C do kernel, que é o oposto ao que é feito pela função i2c_new_device().

 

5. at24_bin_write(): linha 325

 

Essa função é chamada pelo kernel quando uma operação de escrita é realizada sobre o arquivo binário criado por sysfs_create_bin_file(). Pode ser um comando echo ou a função de chamada de sistema write().

 

5.1. dev_get_drvdata(), container_of(): linha 334

 

Essa função retorna o ponteiro armazenado previamente por i2c_set_clientdata() em driver_data, executando a seguinte linha: return dev->driver_data. Ela precisa de um ponteiro para struct device. Como não temos nenhum ponteiro para struct device dentro de at24_bin_write(), precisamos de algum artifício para recuperar esse ponteiro. Isso é feito justamente por container_of(), que aproveita o ponteiro struct kobject *kobj vindo de at24_bin_write e recupera o ponteiro para struct device. Agora que temos um ponteiro válido na variável at24, chamamos at24_write(at24, buf, off, count).

 

5.2. at24_write(): linhas 335 e 294

 

Escreve os dados passados em buf na memória I2C, até atingir o número de bytes igual a count. Por enquanto, não iremos falar de mutex_lock() e mutex_unlock().

 

5.2.1. at24_eeprom_write(): linhas 308 e 233

 

Escreve o número máximo de bytes indicado por write_max ou count se for menor. Primeiro é recuperado o endereço do cliente por meio de at24_translate_offset(). Depois o valor de count é limitado por at24->write_max. A variável msg contém os parâmetros que serão passados para i2c_transfer(). O membro msg.buf recebe a memória alocada em at24_probe() por at24->writebuf. Na posição zero, msg.buf[0], é guardado o endereço de memória a ser escrito. E nas posições posteriores são guardados os dados a serem escritos, por meio de memcpy(). E msg.len guarda o número de bytes a serem escritos, incluindo o endereço mais os dados (i + count).

 

5.2.2. msecs_to_jiffies(): linha 273

 

Converte o valor passado em milissegundos para jiffies. A variável global jiffies mantém o número de ticks que ocorreram desde que o sistema foi inicializado. Tick, por sua vez, refere-se a uma interrupção do timer do sistema que, por padrão em processadores ARM, ocorre a cada 1 ms. Considerando que a variável write_timeout é por padrão 25, quando fazemos

 

 

significa um tempo de 25 ms a partir do momento em que a função está sendo chamada.

 

5.2.3. i2c_transfer(): linha 277

 

Essa função é quem de fato faz a transferência de bytes no barramento I2C, tanto escrita como leitura. A operação de escrita ou leitura é indicada no membro msg.flags, sendo que zero indica escrita. Veja na linha 257 (msg.flags = 0). Ela precisa de um ponteiro para struct i2c_adapter o qual indica o driver do controlador I2C, um ponteiro para struct i2c_msg que indica a mensagem para transferir e um inteiro para indicar o número de mensagens para transferir.

 

5.2.4. time_before(t1, t2): linha 289

 

Testa se o tempo atual t1 alcançou um tempo posterior t2. Se o tempo posterior não foi atingido, retorna verdadeiro. O primeiro parâmetro é o jiffie atual e o segundo parâmetro, o jiffie posterior.

 

6. at24_bin_read(): linha 214

 

Essa função é chamada pelo kernel quando uma operação de leitura é realizada sobre o arquivo binário criado por sysfs_create_bin_file(). Pode ser um comando cat ou a função de chamada de sistema read(). As funções chamadas em at24_bin_read() são as mesmas chamadas em at24_bin_write() com pequenas diferenças. Vejamos então apenas as funções em que há diferenças.

 

6.1. at24_eeprom_read(): linhas 197 e 128

 

Ler o número de bytes máximo indicado por io_limit ou count se menor. Nessa função temos um array struct i2c_msg msg[2], que irá guardar duas mensagens. Na primeira mensagem, indicada por msg[0], temos a operação de escrita, pois msg[0].flag foi omitido e é inicializado com zero. E na segunda mensagem, que é indicada por msg[1], temos a operação de leitura. Veja na linha 154 (msg[1].flags = I2C_M_RD). As duas mensagens são transferidas quando indicamos 2 no último parametro de i2c_transfer(). Veja na linha 167 (i2c_transfer(client->adapter, msg, 2)). Isso é necessário pois a operação de leitura de uma memória requer que primeiro se escreva o endereço que irá ser lido, para então começarmos a ler. As outras funções são as mesmas de at24_eeprom_write().

 

7. module_param(): linhas 60 e 68

 

Essa macro é usada para que o usuário possa passar parâmetros para o módulo quando este é carregado no sistema. Para mais informações veja aqui. Associado a essa macro, temos MODULE_PARM_DESC(), que fornece uma descrição do parâmetro. Os parametros são armazenados em variavéis globais.

 

 

Código do Makefile do device driver

 

O código do Makefile é praticamente o mesmo do que foi usado neste artigo. Porém esse driver foi testado na placa Raspberry Pi 2, e por este motivo, aponta para o código fonte do kernel da Broadcom. Mas como dito antes, pode ser testado em qualquer placa.

 

Para compilar o driver digite:

 

 

Agora estamos prontos para testar driver!

 

 

Exemplo de uso do driver I2C

 

A fim de que o driver funcione, é necessário adicionar informações para que o driver “saiba” que memória será usada. Isso pode ser feito de duas formas: através da estrutura i2c_board_info e a função i2c_register_board_info() para registrar as informações necessárias; ou através de um simples código de Device Tree.

 

Para a primeira abordagem, veja este exemplo.

 

Vejamos como proceder para o caso do Device Tree. Se você está usando a Raspberry 2, abra o arquivo bcm2709-rpi-2-b.dts, e dentro de bloco da i2c1, coloque:

 

 

Assim, o código completo do bloco i2c1 deve ficar assim:

  

 

Este código é para a antiga memória at24c512 da Atmel. Mude conforme a memória que você for testar. Você pode usar esse códido para praticamente qualquer placa com suporte a Device Tree, como a Beaglebone Black e outras. Agora recompile o Device Tree usando:

 

 

Depois transfira o arquivo de Device Tree compilado para a placa. No caso da Raspberry 2, será bcm2709-rpi-2-b.dtb, que deve ser colocado na pasta /boot do SD-Card.

 

Agora finalmente podemos testar a leitura e escrita da mémoria I2C. Digite os seguintes comandos, como root:

 

 

O resultado deveria ser algo como:

  

 

 

Exemplo de software

 

Agora vejamos um exemplo de software para testar a leitura e escrita da memória através do driver I2C.

 

Código:

 

 

Salve o código fonte como i2c_teste.c. Para fazer a compilação na própria Raspberry, digite:

 

 

Para compilar no PC, fazendo uma compilação cruzada, digite:

 

 

Observe que as funções de chamada de sistema open(), read(), write() e close(), mostradas na figura acima (ações do usuário), são usadas no software de exemplo. Mas elas também são usadas nos comandos echo e hexdump, basta verificar no código fonte de cada um.

 

E assim concluímos mais um artigo sobre device drivers para linux embarcado. Para o próximo artigo, veremos um driver SPI.

 

 

Referências

 

[1] Linux Device Drivers - Diferenças entre Drivers de Plataforma e de Dispositivo

[2] Driver at24.c

[3] Exemplo de driver para Linux Embarcado

[4] Pré-processador C – Parte 1

[5] Introdução ao uso de Device Tree e Device Tree Overlay em Sistemas Linux Embarcado

[6] The Linux Kernel Module Programming Guide

[7] Raspberry Pi I2C 256K EEPROM Tutorial

 

Fonte da imagem destacada: http://openelectronicsproject.blogspot.com.br/2015/08/i2c-protocol.html

Outros artigos da série

<< Comunicação SPI em Linux
NEWSLETTER

Receba os melhores conteúdos sobre sistemas eletrônicos embarcados, dicas, tutoriais e promoções.

Obrigado! Sua inscrição foi um sucesso.

Ops, algo deu errado. Por favor tente novamente.

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.

Vinicius Maciel
Cursando Tecnologia em Telemática no IFCE. Trabalho com programação de Sistemas Embarcados desde 2009. Tenho experiência em desenvolvimento de software para Linux embarcado em C/C++. Criação ou modificação de devices drivers para o sistema operacional Linux. Uso de ferramentas open source para desenvolvimento e debug incluindo gcc, gdb, eclipse.