quinta-feira, maio 24, 2012

Programação em C no AVR: A avr-libc e Outros Detalhes

Quem conhece C apenas pelo aprendizado da linguagem (por exemplo através do clássico K&R) tem grandes surpresas quando começa a programar para microcontroladores. Em um ambiente embarcado, com recursos limitados e tendo que interagir diretamente com o hardware, as coisas são um pouco diferentes. É o que veremos neste post.


No caso do avr-gcc, a avr-libc não apenas fornece um subconjunto da biblioteca padrão da linguagem C, mas também:
  • Definições e rotinas para interação com o hardware
  • O código de iniciação (startup)
  • Documentação de vários aspectos importantes do avr-gcc
A documentação está disponível no site da avr-libc.

Seções da Memória

As seções de memória são usadas para separar as várias informações geradas pelo compilador. No avr-gcc as mais importantes são:
  • .text contém as instruções geradas, esta seção será mapeada nos endereços da Flash e gravada lá pelo avrdude.
  • .data contém as variáveis estáticas com iniciação. Esta seção será mapeada em endereços da Ram, mas uma cópia dela será gravada na Flash. No começo da execução os valores na Flash serão copiados para a Ram, iniciando as variáveis.
  • .bss contém as variáveis estáticas não iniciadas. Esta seção será mapeada em endereços da Ram. No começo da execução esta área da Ram normalmente será zerada.
Estas são as seções usadas automaticamente pelo avr-gcc.  Para forçar que um dado (ou código) vá para uma seção diferente existe a construção:


__attribute__ ((section (".secao")))

Por exemplo, pode-se colocar dados na seção .eeprom que será mapeada em endereços da EEProm; o comando objcopy pode ser usado para extrair estes dados para posterior gravação através do avrdude. O avr-gcc não irá gerar automaticamente código para ler ou escrever na eeprom, é preciso usar as funções eeprom_* da biblioteca (mais detalhes aqui).

Colocando Constantes na Flash

Uma das principais dificuldades do uso de C em microcontroladores é que vários deles, como o AVR, seguem a chamada "arquitetura Harvard" onde as memórias para dados e código são fortemente separadas.

Um desejo óbvio do programador é colocar dados constantes (como mensagens e tabelas fixas) diretamente na Flash, ao invés de consumir Ram para o uso e Flash para guardar o valor inicial (que nunca será alterado). Vários compiladores utilizam o modificador padrão const para isto e geram automaticamente o código necessário para o acesso aos dados no Flash. Os desenvolvedores do avr-gcc assumem uma posição mais "purista" e requerem um procedimento mais complexo.

Para colocar um dado na Flash utiliza-se o __atrribute__. Para simplificar existe uma macro PROGMEM, definida em <avr/pgmspace.h>:

unsigned char tabela[] PROGMEM = { 0x01, 0x02, 0x04, 0x08, 0x10 };

Para acessar um dado na Flash é necessário usar a função pgm_read_byte passando o endereço do byte a ler:

dado = pgm_read_byte (&(tabela[i]));

Se isto não parece confuso, veja na documentação como colocar na Flash e usar uma tabela de strings.

Interagindo Diretamente Com o Hardware

No AVR, como na maioria dos microcontroladores, o controle do hardware é feito através de bits em registradores, que são posições de memória. Quem programa no ambiente Arduino está acostumado com funções amigáveis como digitalWrite(pino, nivel). Com a avr-libc você terá bem mais trabalho.

O include <avr/io.h> traz definições dos registradores e bits para o modelo definido na opção -mmcu da linha de comando do avr-gcc. Isto é feito incluindo automaticamente um arquivo específico para o modelo (por exemplo, avr/iom328p.h para o ATmega328P).

Os nomes usados são os contidos no manual (ou datasheet) da Atmel (que é bom você ter em mão). Para atrapalhar um pouco mais, os bits estão definidos na forma de número e não máscara. Explicando com um exemplo: o bit TOIE0 do registrador TIMSK0 define se o timer 0 irá gerar uma interrupção quando ocorrer um overflow (isto você descobre no datasheet). TOIE0 está definido (no iom328p.h) como 0, que é o número do bit (ou seja, é o menos significativo). Para ativarmos o bit, precisamos converte o número do bit (0) na máscara correspondente (que é 0x01), o que é feito pela macro _BV().  Isto nos leva a

TIMSK0 = _BV(TOIE0);

Um digitalWrite (4, LOW) do Arduino corresponde a

PORTD &= ~_BV(PD4);

já que o "digital 4" do Arduino corresponde ao PD4 (bit 4 da porta D) no ATmega328. O ~ irá gerar a máscara negada (com 0 no bit que estamos interessados) e o &= irá zerar o bit no registrador PORTD.


Codificando Rotinas de Interrupção

Outro ponto não previsto na linguagem C é como indicar que uma rotina fará o tratamento de uma interrupção. Neste ponto o avr-gcc é bem simples:

#include <avr/interrupt.h>

ISR(vetor)
{
    // código para tratar a interrupção
}

vetor é o nome da interrupção (veja a lista aqui). A macro ISR é suficiente para incluir as instruções adicionais necessárias no início e fim da rotina e para colocar o endereço da rotina no endereó apropriado da Flash.

O include de avr/interrupt.h também define duas macros, sei() e cli(), para habilitar e inibir globalmente as interrupções.

Concluindo

A avr-libc, junto com o avr-gcc e os outros componentes do tool-chain, dá acesso a praticamente todos os recursos dos microcontroladores AVR. Nem sempre a forma de uso é simples, mas uma boa leitura da documentação e uma eventual busca de exemplos na internet resolve todos os problemas.

2 comentários:

Thiago disse...

Boa noite, poderia me indicar, caso exista, outro compilador que faça a mesma função do avr-gcc?
Obrigado

Daniel Quadros disse...

Não tenho experiência com eles, mas existem vários compiladores comerciais para o AVR. O da IAR é bastante elogiado. Encontrei também o mikroC. Esta página tem uma lista bem desatualizada. O Atmel Studio utiliza o gcc.