Para começar, a gente precisa olhar a plaquinha Arduino (neste caso, a Uno) pelo que ela realmente é: uma forma simplificada e prática de usar o microcontrolador ATmega328P. Então o próximo passo é ir lá no site da Atmel e baixar o datasheet do microcontrolador (aqui em versão completa, são 36MB), e depois disso baixar também a referência do conjunto de instruções da família AVR (à qual o Atmega328P pertence). Com estes dois documentos, já conseguimos entender bem como é que o bichinho funciona, e como são as suas instruções. Imprima e leia algumas vezes.
O próximo passo é instalar as ferramentas que serão usadas para gravar o programa na plaquinha e para manipular os arquivos de código. O bom é que elas são as mesmas ferramentas usadas pela interface IDE do Arduino: o avrdude para gravar na flash do ATmega328 e as GNU binutils (cross-compilando para AVR). Se você tem o ambiente do Arduino instalado (no Fedora, sudo yum install arduino) está tudo pronto. Uma maneira legal de começar a sujar as mãos com bits crus devagarinho é compilar um programa de exemplo qualquer na interface do Arduino, e depois ir no /tmp olhar os arquivos intermediários no diretório de build. Arrisque-se a digitar "avr-objdump -d foobar.elf" e olhar o código gerado pelo compilador. :-)
Para o meu primeiro teste eu quis fazer um pouco mais do que o tradicional Hello World do Arduino, que é piscar um led. Então resolvi fazer um programa que pisque 8 leds em zigue-zague, usando 8 pinos digitais. Decidi usar os primeiros 8 pinos, D0 a D7, porque eles são todos bits da mesma porta D para o ATmega328, o que facilita a programação. O programa basicamente seta um bit em um registrador e o envia para a porta, esperando um delay rápido e depois deslocando o registrador um bit para a direita e escrevendo na porta, em laço até zerar o registrador. Depois disso, recarrega um valor com um bit setado no registrador e faz o mesmo deslocando para a esquerda, até zerar (quando o bit "cair fora" do registrador:-).
Li algumas páginas relacionadas a sintaxe padrão de assembly AVR, e depois resolvi fazer tudo manualmente, em um formato livre. Então abri o vim e criei este arquivo, que chamei de teste.asm.
; Teste de programação do Arduino "assemblando" manualmente os opcodesClaro que eu não saí digitando tudo direto. Eu comecei cada linha com dois tabs, e comecei a listar as instruções aos poucos, pensando em como ia usar os registradores. Acabei decidindo usar os registradores "altos" (r16 em diante) para poder usar a instrução LDI (Load Immediate), que só manipula de r16 a r31. Quando eu tiver mais experiência com o conjunto de instruções acho que vou começar a usar mais os registradores baixos também. Depois de prontas as instruções, preenchi a primeira coluna com os endereços, e então fui aos poucos olhando cada uma das instruções na referência do AVR Instruction Set.
; DDRD = 0a
; PORTD = 0b
; Os opcodes precisam ser convertidos para little-endian no .bin
addr opcode instruction
0000 ef0f ldi r16, 0xff ; programa porta D como saídas
0001 b90a out DDRD, r16
0002 e000 ldi r16, 0x00 ; limpa registradores de contagem dos delays
0003 e010 ldi r17, 0x00
0004 e440 ldi r20, 0x40 ; inicio
0005 b94b out PORTD, r20 ; loop1, led para a direita
0006 e024 ldi r18, 0x04
0007 9503 inc r16 ; delay1
0008 f7f1 brne delay1
0009 9513 inc r17
000a f7e1 brne delay1
000b 952a dec r18
000c f7d1 brne delay1
000d 9546 lsr r20
000e f7b1 brne loop1
000f e042 ldi r20, 0x02
0010 b94b out PORTD, r20 ; loop2, led para a esquerda
0011 e024 ldi r18, 0x04
0012 9503 inc r16 ; delay2
0013 f7f1 brne delay2
0014 9513 inc r17
0015 f7e1 brne delay2
0016 952a dec r18
0017 f7d1 brne delay2
0018 0f44 lsl r20
0019 f7b1 brne loop2
001a cfe5 rjmp inicio
Aqui cabem duas observações interessantes:
- O AVR é um processador RISC de 8 bits, e os opcodes têm um tamanho fixo de 16 bits (algumas instruções precisam 32, mas isso fica como tema para casa). Embora na referência eles sejam demonstrados como números binários de 16 bits, na hora de gravar na flash eles precisam ser convertidos para little-endian. Minha primeira tentativa de criar o arquivo "binário" com o código acabou dando errado, e tive que digitar tudo de novo invertendo os bytes de cada opcode. :-)
- Embora cada instrução ocupe 2 bytes, o Program Counter do AVR aponta para instruções, e não para bytes. Isso significa que os offsets de branches e jumps são offsets em instruções, e não em bytes. Então para pular 4 instruções pra cima, se usa um offset de -4, mesmo que na verdade se esteja pulando 8 bytes para trás. Pra completar a confusão, o objdump mostra o disassembly com endereços de bytes, e ele mostra os opcodes invertidos, já em formato little-endian.
Feita a tradução dos mnemônicos para os opcodes binários (e calcular os offsets de branches contando em hexa em complemento de 2 nos dedos), agora a tarefa é digitar todos os opcodes em um arquivo, que será o "binário compilado" desse nosso esforço manual. Eu uso o hexedit, que é simples e direto, mas qualquer editor hexadecimal serve. Lembre-se de digitar os opcodes convertendo para little-endian. Fazendo um hexdump do "código compilado" teste.bin fica assim:
$ hexdump -C teste.binNo total são 54 bytes, para 27 instruções de máquina. Eu completei com uns zeros no final, só para fechar 64 bytes e ficar tudo alinhado. :) O opcode do NOP em AVR é 0x0000 então tá tranquilo. :-)
00000000 0f ef 0a b9 00 e0 10 e0 40 e4 4b b9 24 e0 03 95 |........@.K.$...|
00000010 f1 f7 13 95 e1 f7 2a 95 d1 f7 46 95 b1 f7 42 e0 |......*...F...B.|
00000020 4b b9 24 e0 03 95 f1 f7 13 95 e1 f7 2a 95 d1 f7 |K.$.........*...|
00000030 44 0f b1 f7 e5 cf 00 00 00 00 00 00 00 00 00 00 |D...............|
Um passo interessante, agora, é transformar esse arquivo contendo opcodes de AVR em um arquivo que o avr-objdump entenda e possa desassemblar, para que a gente confira o código e principalmente os offsets de branches, que o objdump nos ajuda a entender. Não é um passo necessário, mas é bom pra ter certeza que o código que montamos está realmente fazendo o que pretendemos que ele faça. Eu não consegui achar uma maneira de convencer o objdump a desmontar o binário "cru" diretamente, então usei o objcopy para transformá-lo em um ELF comum:
$ avr-objcopy -I binary -O elf32-avr --rename-section .data=.text,contents,code teste.bin \
teste.elf
$ avr-objdump -d teste.elf
teste.elf: file format elf32-avr
Disassembly of section .text:
00000000 _binary_teste_bin_start:
0: 0f ef ldi r16, 0xFF ; 255
2: 0a b9 out 0x0a, r16 ; 10
4: 00 e0 ldi r16, 0x00 ; 0
6: 10 e0 ldi r17, 0x00 ; 0
8: 40 e4 ldi r20, 0x40 ; 64
a: 4b b9 out 0x0b, r20 ; 11
c: 24 e0 ldi r18, 0x04 ; 4
e: 03 95 inc r16
10: f1 f7 brne .-4 ; 0xe _binary_teste_bin_start+0xe
12: 13 95 inc r17
14: e1 f7 brne .-8 ; 0xe _binary_teste_bin_start+0xe
16: 2a 95 dec r18
18: d1 f7 brne .-12 ; 0xe _binary_teste_bin_start+0xe
1a: 46 95 lsr r20
1c: b1 f7 brne .-20 ; 0xa _binary_teste_bin_start+0xa
1e: 42 e0 ldi r20, 0x02 ; 2
20: 4b b9 out 0x0b, r20 ; 11
22: 24 e0 ldi r18, 0x04 ; 4
24: 03 95 inc r16
26: f1 f7 brne .-4 ; 0x24 _binary_teste_bin_start+0x24
28: 13 95 inc r17
2a: e1 f7 brne .-8 ; 0x24 _binary_teste_bin_start+0x24
2c: 2a 95 dec r18
2e: d1 f7 brne .-12 ; 0x24 _binary_teste_bin_start+0x24
30: 44 0f add r20, r20
32: b1 f7 brne .-20 ; 0x20 _binary_teste_bin_start+0x20
34: e5 cf rjmp .-54 ; 0x0 _binary_teste_bin_start
...
Legal, com isso já consigo verificar que eu calculei os opcodes corretamente, e que meus offsets de branches e jumps estavam certos. Agora só falta gravar no Arduino. Para isso, temos que converter o binário em um arquivo no formato Intel Hex, com extensão .hex. Este é o formato usado pelo avrdude, que é quem conversa com o bootloader do Arduino e grava o nosso programa lá. Os comandos abaixo deram conta do recado:
$ avr-objcopy -I binary -O ihex teste.bin teste.hex
$ avrdude -P /dev/ttyUSB0 -p ATmega328P -b 115200 -c arduino -U flash:w:teste.hex:i
O avrdude gera bastante saída na tela, dá pra ir acompanhando os passos da gravação. Note que eu usei um Garagino com uma porta serial FTDI que o acompanha, e ela é detectada como /dev/ttyUSB0. Uma plaquinha Arduino Uno, por exemplo, é detectada como /dev/ttyACM0, então o comando acima deverá ser adaptado. Feita a programação e montado o circuito, voilà! Está tudo funcionando como esperado, em apenas 27 instruções de código!
Estou muito feliz de finalmente ter escovado estes bits, que estavam me esperando desde que comprei a primeira Arduino Uno. Só não tinha dado tempo ainda de pensar num projetinho simples porém não tão trivial, sentar e começar a codar. Levei horas codificando os opcodes manualmente, e é claro que não pretendo mais fazer isso. Meus próximos testes serão utilizando o GNU Assembler para facilitar as coisas. Estou commitando estes e outros testes com Arduino neste repositório no github, para facilitar a publicação e utilização pelos interessados.
Ah, antes que me esqueça: desde que a gente não sobrescreva o bootloader do Arduino, que ocupa os últimos 512 bytes da flash, vai continuar funcionando tudo normalmente, e a placa continua compatível com a IDE do Arduino. Continua sendo possível criar e gravar um programa pelas vias normais, usando a IDE e clicando botõezinhos. :-) Se o nosso programa tiver menos do que 32256 bytes não vai sobrescrever o bootloader e tá tudo certo.
Update 2014-06-20: Na verdade nem precisa converter o arquivo binário para ihex. O avrdude também pode ler arquivos binários sem formato, especificando a opção "raw". Agora mesmo eu mudei um pouco o padrão dos leds e escrevi o teste.bin direto usando "-U flash:w:teste.bin:r". O ":r" no final é que indica o tipo, sendo 'i' Intel Hex, e 'r' raw. Só o que se perde com isso é a possibilidade de especificar o endereço de carga. O ihex pode especificar endereços, para o caso de querermos gravar um conteúdo na flash a partir de um ponto específico. O raw sempre vai ser gravado a partir de 0 na flash. Bom saber que nem precisa fazer a dança do objcopy, embora seja mais reconfortante fazer um objdump e confirmar o código antes de gravar. :-)
No comments:
Post a Comment