quarta-feira, 4 de novembro de 2015

Primeiro programa para a placa STM32F0Discovery

No último post, vimos a arquitetura do processador ARM Cortex, da qual destacamos o Vector Table no endereço 0x00000000. A primeira entrada aponta para o início da pilha e, a partir da segunda, temos os endereços das funções de exceção. A primeira delas é a exceção de RESET do processador. Ela é a que usaremos neste primeiro programa. Eis o código:

  1. // Primeiro programa para ARM Cortex-M0
  2. .code 16 // Define código como THUMB
  3. .globl _start // Necessario para o linker
  4. .equ STACK_INIT,0x020002000
  5. .text // Inicio da área de código
  6. _start:
  7. // Inicio do Vector Table
  8. .word STACK_INIT // Inicio da pilha
  9. .word reset_handler // Endereco do Reset Handler
  10. // define o label como função para que o linker
  11. // resolva como código THUMB
  12. .type reset_handler, function
  13. reset_handler: // inicio do tratamento do reset
  14. mov r0, #3 // carrega r0 com o valor 3
  15. mov r1, #4 // carrega r1 com o valor 4
  16. add r2, r1, r0 // soma r0 e r1 guardando em r2
  17. _stop: b . // loop infinito

Vamos aos detalhes.

Como visto em um post anterior, usaremos o conjunto de ferramentas ou toolchain da GNU portanto sua sintaxe deve ser respeitada. Há muita documentação na internet do GNU Assembly e algumas delas estão nas referências. Primeiramente, um código fonte assembly tem vários tipos de instruções:

  • Instruções de máquina do dispositivo final sendo, no nosso caso: processador ARM Cortex M0;
  • Instruções para o próprio montador (assembler);
  • Instruções para o linker;
  • Comentários.
Os comentários são iniciados por @ ou por // onde o montador ignora o resto da linha. Pode ser também por um bloco delimitado por /* e */ podendo ser de mais de uma linha inclusive.
As instruções de máquina são específicas, no nosso caso, para o processador ARM Cortex M0, se resumindo a 56 instruções as quais podem ser vistas na figura abaixo:



O formato geral da sintaxe desse montador é a seguinte:


Onde label: é um rótulo para facilitar a identificação do endereço da instrução; mnemonic é ó códico mneumonico representando a instrução conforme as tabelas acima; operad1,operad2,... são os argumentos da instrução e dependente em número e formato da instrução propriamente dita.

Finalmente, temos as instruções para o montador e para o linker, ou diretivas. Elas começam com ponto e serão vistas e explicadas ao longo do seu uso.

Voltemos então ao programa.

Na linha 3, temos a primeira diretiva: .code 16  a qual diz ao montador para usar o conjunto de instruções de 16 bits Thumb.
Na linha 4, a diretiva .globl _start diz ao linker para considerar o símbolo _start como global. Isso é necessário pois o linker obriga a existência de um símbolo _start que será definido adiante.
Na linha 6, a diretiva .equ STACK_INIT,0x020002000 defini para o montador o símbolo STACK_INIT atribuindo a ele o valor 0x020002000 que, no nosso caso, é o endereço inicial da pilha.
Na linha 8, a diretiva .text diz ao linker que, nesse endereço, inicia-se o código executável do programa o qual reside na memória flash. Em contrapartida a essa diretiva, há a diretiva .data que diz ao linker qual é o inicio da área de memória volátil para uso do programa.
Na linha 9, finalmente, o símbolo _start é definido no formato de 'label:' e refere-se ao inicio do código propriamente dito.

Uma vez definido o tipo de instrução e o endereço inicial do código, podemos começar a definir o Vector Table. Até aqui, nenhuma posição de memória foi consumida.

Na linha 13, a diretiva .word ocupa uma palavra de 32 bits e define seu conteúdo como, no caso, o valor definido no símbolo STACK_INIT. A primeira posição do Vector Table foi preenchido com o endereço inicial da pilha.
Na linha 14, a nova diretiva .word define o endereço da função de tratamento da exceção de RESET, no caso, o símbolo reset_handler que será definido adiante. O uso de símbolos permite a flexibilidade de se postergar a codificação de funções ou mesmo relocá-las de posição sem dano ao funcionamento do código.
Na linha 19, temos a diretiva .type reset_handler, function necessária para dizer ao ao linker que o endereço referenciado pelo símbolo reset_handler é uma função, ou melhor, um código Thumb. Efetivamente, essa diretiva liga o bit menos significativo do endereço referenciado por esse símbolo sempre que ele for utilizado.
A partir da linha 21, está definida a função de tratamento da exceção de RESET a partir da definição do símbolo reset_handler como label. Essa função é simples contendo apenas 4 instruções:

  • Linha 23, carrega o valor 0x03 no registrador 0;
  • Linha 24, carrega o valor 0x04 no registrador 1;
  • Linha 25, soma os valores contidos nos registradores 0 e 1 guardando o resultado no registrador 2;
  • Linha 27, entra em loop infinito desviando para o endereço da própria instrução (representado como argumento ponto).
Para evitar uma mensagem de warning no montador, o programa deve encerrar com uma linha em branco.

Uma pausa antes de começar a gerar os códigos.

O processador ARM Cortex possui registradores os quais- são áreas de 32 bits dentro do processador e são usadas para controle da aplicação, do estado do processador e para a aplicação. São eles:


Os registradores que usamos: R0, R1 e R2, são de uso geral para aplicações assim como os registradores R3 a R12. O registrador R13 contem o ponteiro pra última posição da pilha utilizada. No nosso caso, ao iniciar o processador, esse registrador terá o valor definido por STACK_INIT, ou 0x20002000. O registrador R14 possui o endereço de retorno quando o processador desvia para executar funções. Ao iniciar, possui o valor 0xffffffff. O registrador R15 ou program counter, possui o endereço da próxima instrução a ser executada. Ao iniciar, é carregado com o endereço apontado pelo conteúdo da função de tratamento de RESET do Vector Table. No nosso caso, ao iniciar, possui o endereço definido por reset_handler. Os demais registradores serão vistos em posts futuros.

Vamos executar o nosso código.

Primeiramente, salvemo o programa como rst_0.s. A extensão .s representa um programa assembly.

Para compilá-lo, executemos o comando abaixo em uma janela de linha de comando do MS-Windows:

arm-none-eabi-as -a -o rst_0.o rst_0.s

O comando arm-none-eabi-as é o assembler do nosso ferramental instalado. Como parâmetros, estamos passando -a que indica ao assembler para gerar a listagem do que ele está montando; -o rst_0.o para gerar como saída o arquivo rst_0.o e, rst_0.s indica o arquivo fonte: no nosso caso, o nosso programa. A compilação gera uma saída como a da figura abaixo:



Como pedimos para gerar listagem da montagem, algumas informações estão presentes na figura: o símbolo STACK_INIT está definido como valor o absoluto 20002000. O símbolo _start está definido para o endereço de inicio ou zero. O símbolo reset_handler está definido com o valor 00000008 que é endereço da função de tratamento de RESET que codificamos.
Podemos ver na listagem de montagem que o Vector Table está preenchido para a sua primeira entrada que é o ponteiro inicial da pilha e este está no formato little-endian (que é o default do montador). Já o endereço da função de tratamento de RESET não está preenchida e o será quando executarmos o linker com o comando abaixo:

arm-none-eabi-ld -Ttext=0x00000000 -o rst_0.elf rst_0.o

O linker é o comando arm-none-eabi-ld e recebe como parâmetros -Ttext que resolve a diretiva .text do nosso programa para o endereço 0x00000000. O parâmetro seguinte: -o rst_0.elf indica ao linker para gerar um arquivo de saída no formato elf chamado rst_0.elf. Finalmente o último parâmetro é o arquivo de entrada para o linker que é o arquivo gerado na nossa compilação: rst_0.o.

O formato elf (Executable and Linking ou Extensible Linking Format) é um formato que abriga o programa executável além dos símbolos usados em seu código fonte úteis para o processo de depuração.

Se tudo correr sem problemas, o comando acima não gera relatório.

Para vermos vermos os símbolos resolvidos pelo linker, usamos o comando:

arm-none-eabi-nm rst_0.elf

A saída é a seguinte, onde podemos ver os símbolos de nosso programas:


Para executarmos o programa no microcontrolador apenas nos interessa o código binário gerado, não os símbolos. Para tanto precisamos extrair do arquivo rst_o.elf o código binário. Fararemos isso com o comando abaixo:

arm-none-eabi-objcopy -O binary rst_0.elf rst_0.bin

O comando arm-none-eabi-objcopy é um utilitário de cópia de objetos e estamos solicitando que o formato de saída seja binário e gere, a partir do arquivo rst_0.elf, o arquivo rst_0.bin. Esse comando também não gera geratório de saída. Utilizando um editor hexadecimal para observar o arquivo de saída termos:


Podemos ver, no endereço zero, o endereço inicial da pilha (0x20002000) no formato little-endian (0x00200020). Já na no endereço 0x04, temos o endereço do reset_handler já com o bit menos significativo ligado (0x00000009) no formato little-endian também (0x09000000) indicando que é um código Thumb. A partir do endereço 0x08 temos a sequência 0x032004210A18FEE7 que é o código objeto da função reset_handler que codificamos.

Vamos agora gerar uma imagem completa da memória flash do microcontrolador presente na placa STM32F0Discovery que tem o tamanho de 16 KBytes. Para tal, usaremos dois comandos. O primeiro gera uma imagem zerada de 16 Kbytes e o segundo copia nosso arquivo binário para a posição zero dessa imagem.

dd if=/dev/zero of=flash.bin bs=4096 count=16

O comando acima copia o arquivo default de zeros binários /dev/zero para o arquivo de saída flash.bin com 16 blocos de 4096 bytes cada. Teremos um arquivo de imagem da memória flash com 64 KBytes zerados.

dd if=rst_0.bin of=flash.bin bs=4096 conv=notrunc

Finalmente, copiamos o conteúdo do arquivo binário rst_0.bin para a imagem flash com o comando acima. Será copiado um bloco de 4096 bytes e o arquivo de saída não será truncado. O resultado está abaixo:


Pronto, já temos um arquivo de imagem de memória flash para carregar no microcontrolador. No software STM32 ST-Link Utility conectemos com o microcontrolador clicando no botão .

Na sequência, escolher a função: file -> open file  e escolher nosso arquivo flash.bin.


Pronto, a imagem foi carregada no utilitário:


Para carregar na memória flash do microcontrolador o arquivo aberto, usar o botão de programação: .


O endereço de carga vem preenchido com o endereço da flash que, no caso, é 0x08000000. Já vimos anteriormente que o endereço da memória flash desse microcontrolador é mapeada nesse endereço e também no endereço 0x00000000. Podemos manter preenchido ou mudá-lo. Para efetivar a carga, clicar no botão start.

Podemos ver nosso programa mostrado na tela. Como o utilitário converte o formato little-endian para big-endian para facilitar a nossa compreensão dos endereços, o código da função reset_handler a partir da posição 0x08000008 parece estar com os bytes invertidos. Basta mudarmos o display para 8 bits e tudo fica como realmente está na memória.


Vamos executar o código. Entremos na opção Target -> MCU Core. Teremos algo como a imagem abaixo:

Os registradores estão, aparentemente, com valores aleatórios e o processador esta em estado Halted (parado). Como o inicio da execução se dá com o RESET, cliquemos no botão System Reset.



O estado do processador muda para Running (executando) e não é possível ver o valor de qualquer registrador, Para tal, precisamos parar o processador clicando no botão Halt.


O processador volta ao estado Halted e podemos ver seus registradores. Como programado, o registrador R0 possui o valor 3, o R1 o valor 4 e o registrador R3 a soma dos dois, ou seja, 3+4=7. Notamos também que o registrador R13 aponta para o inicio da pilha conforme programamos. Notamos ainda que, o registrador R15 ou PC (program counter) possui o valor 0x0e que é o endereço da próxima execução ou o símbolo _stop do nosso programa (o loop infinito). Os demais registradores R3 a R12 e R14 possuem o valor inicial 0xffffffff.

Com isso concluímos a codificação e execução do nosso primeiro programa para o microcontrolador STM32F051R8T6 que equipa a placa STM32F0Discovery. Apesar de simples e de não ter uma utilidade prática, ele serviu para assimilar muitos conceitos.

No próximo post simplificaremos o processo de geração de objetos e imagem através do comando MAKE.

REFERÊNCIAS
http://tigcc.ticalc.org/doc/gnuasm.html
http://www.coranac.com/tonc/text/asm.htm
"Cortex-M0 Devices Generic User Guide", ARM
"The Definitive Guide to the ARM Cortex-M0",Joseph Yiu
https://pt.wikipedia.org/wiki/ELF

Nenhum comentário:

Postar um comentário