Contando instruções para verificar a viabilidade de um projeto

Nota: Este post é um registro dos primeiros estudos para um projeto que estou envolvido (e é mais um registro pessoal). Aqui apresento o problema inicial e vejo se existe a possibilidade da minha idéia vir a funcionar.

1. Sobre o problema:

Um conhecido possui uma máquina fabricada nos anos 80 que executa determinada tarefa. Tal máquina possui como controlador principal um processador equivalente ao MOS 6502, com circuito tipico da época com entradas (digitais e analógicas) e saídas, interface com o usuário (até quatro simultâneos) não muito avançada e pouca memória RAM. 

A programação é feita com "cartões" (placas de circuito impresso) com memórias EPROM (e/ou RAM em alguns casos), ligadas diretamente ao barramento de dados, endereços e controle. Troca-se a placa para se trocar o programa que a máquina irá executar (feitos em Assembly). Acontece que as tais placas de programação vão se desgastando com o tempo, se perdem, queimam, apresentam problemas e tem que ser trocadas. Como o hardware é antigo é um pouco complicado encontrar peças para manutenção. Em alguns casos é só trocar a EPROM por uma nova e continua-se a usar a máquina. 

Para facilitar a vida dos usuários pensou-se numa unica placa onde se possa trocar o programa via cartão SD ou por uma porta USB. Tal placa já existe e é fabricada lá fora justamente para atender a esta demanda. Custa por volta de $100 dólares (inclua-se aí o custo de trazer uma destas pra cá e deve dar duas ou três vezes esse valor). 

Antes que me perguntem, existem outras opções:

- Máquinas modernas especificas para este trabalho já existem . Mas não são tão simples, são mais caras que uma antiga usada (cinco a dez vezes mais) e as licenças de software exigem conexão com a rede do fabricante. O sistema é fechado, dificultando que você faça um programa ou modifique um existente para suas necessidades. Fora isso tem um desempenho anos-luz a frente de sua ancestral. Quem tem e usa diariamente nem quer saber da antiga mais.

- Dá pra emular o controle da máquina com um PC. Conheço pessoas que usam esta alternativa e gostam. Poderia ser a melhor opção, mas alguns operadores não gostam de usar um teclado para controle e a tela do PC como UI. Acho que é mais questão de gosto e diria até de saudosismo não usar um PC para isso. Alguns usam também a desculpa do tempo de boot do sistema, já que a máquina original é só ligar e usar.

Mas voltando a este caso especifico, o dono da máquina não quer se livrar dela e quer o projeto de uma placa equivalente a fabricada lá fora (cartão SD + USB).


2. Pré-projeto:


Os mais atentos já devem ter notado que a solução é algo parecido com um emulador de memória. Seria então uma placa nas mesmas dimensões do "cartão de programação" que carregaria um programa de um cartão SD ou outro meio para uma memória que seria acessada pelo processador da máquina. Os fatores limitantes são:
* Como em todo circuito da época as linhas de dados, endereços e controle são de 5V.
* Os componentes devem ser faceis de encontrar por aqui.
* O custo total deve ficar bem abaixo dos $100,00 Dolares. Caso fique maior ou igual compensa comprar a solução de fora.
* O tempo de acesso da memória, segundo o datasheet dos 6502 e equivalentes, é de, no máximo e com uma margem de segurança, 500 ns.

No primeiro rabisco do projeto pensei em um microcontrolador ligado a uma memória RAM externa (qualquer uma hoje tem um Tacc menor que 100ns) e alguns conversores de nível (3.3V para 5V). Em alguns "cartões" a memória é paginada e necessita de alguma lógica extra (feita com CIs TTL nas placas originais). Para estes casos pensei em uma CPLD, mas daí o projeto já ficou grande (não iria caber), custo alto (melhor comprar lá fora) e com componentes complicados de achar (PLD). 

Após alguns anos (!) pensando sobre o assunto eu sempre esbarrava nestes três problemas. Até que ano passado o meu conhecido, dono da máquina, me mostrou uma plaquinha com um microcontrolador SAM3X8E da Atmel que poderia ser a solução (quase) perfeita. O dito microcontrolador roda a 84MHz, tem pinos mais que suficientes para ser ligado (quase) diretamente aos barramentos de dados, endereços e controle da máquina e, mais importante, memória RAM interna de 96kB. Analisando a placa em relação aos quatro limitantes anteriores temos:
* Três ou quatro conversores de nível como o TXB0108 da Texas resolvem o problema do 3.3V vs 5V.
* Ele já tem a placa e os conversores de nível.
* O custo da placa é de $15,00 segundo o dono da máquina. Os conversores de nível são bem baratos ($1,00).
* O tempo de acesso é que vai pegar. Todo o processo do 650X da máquina acessar o barramento de endereços e ter um dado válido tem que levar menos de 500ns.

Será que agora vai? Bom, antes de botar a mão na massa (montar o protótipo) é preciso testar a possibilidade do microcontrolador fornecer os dados ao processador dentro de um Tacc de 500ns.

3. Testes da possibilidade de funcionamento:


O microcontrolador da plaquinha é um ARM Cortex M3 rodando a 84MHz. Isso dá um ciclo de clock de aproximadamente 12ns. Em um mundo ideal e num microcontrolador ideal eu teria 42 ciclos de clock (500ns/12ns) para completar a tarefa de ler um endereço no barramento do 650X da máquina e colocar um dado válido no barramento de dados. O número de ciclos de clock que um ARM gasta por instrução depende de vários fatores e não é muito fácil de calcular. O certo então seria escrever uma função, compilar e olhar o código de máquina (ou assembly) gerado e contar e/ou estimar os ciclos usados.

Para complicar um pouco a plaquinha com o ARM parece ter sido roteada para facilitar as ligações. Isso causou um problema com as linhas de endereços que não ficarão continuas. Tenho um buraco entre dois pinos do microcontrolador no portal de endereços que não são ligados externamente. Para resolver é necessário dois deslocamentos e uma soma no software, e perder alguns ciclos de clock ou usar uma tabela usando o endereço como indexador e gastar muita memória (128kB) mas ganhando alguns ciclos. Como o microcontrolador possui 512kB de Flash optei pela segunda opção.

Para os testes iniciais escrevi umas linhas de código em C já com o caso mais difícil, com paginação de memória. Neste caso seria para substituir uma EPROM 27C256 e alguma lógica com TTLs. O vetor table é a tabela para ajustes dos endereços e o vetor buffer é o conteúdo da EPROM que será emulada. "data" é o valor no barramento de dados e "addrs", claro, a entrada no barramento de endereços. "bank" é responsável pela troca de bancos de memória.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
//Codigo em C
const unsigned short table[65536] = {0};
unsigned char buffer[32768];

int main (void)
{
    unsigned short portc;
    unsigned char data;
    unsigned short addrs;
    unsigned char bank = 0;
  
    for(;;)
    {
        addrs = table[portc];
        if((addrs >= 0x1ff4) && (addrs <= 0x1ffb))
        {
            bank = (addrs & 0x000f) - 4;
        }
        data = buffer[addrs + (bank * 4096)];
    }
    return 1;
}

Para não gastar muito tempo instalando uma IDE para testar, usei o Compiler Explorer para ter uma noção de como ficaria o Assembly gerado pelo GCC. Selecionei o ARM gcc 5.4.1 e coloquei na caixa "compiler options..." o seguinte: "-mtune=cortex-m3". O resultado foi:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
;codigo em ASSEMBLY
table:
buffer:
main:
        str     fp, [sp, #-4]!
        add     fp, sp, #0
        sub     sp, sp, #12
        mov     r3, #0
        strb    r3, [fp, #-5]
.L3:
        ldrh    r3, [fp, #-8]
        ldr     r2, .L4
        mov     r3, r3, asl #1
        add     r3, r2, r3
        ldrh    r3, [r3]        @ movhi
        strh    r3, [fp, #-10]  @ movhi
        ldrh    r3, [fp, #-10]
        ldr     r2, .L4+4
        cmp     r3, r2
        bls     .L2
        ldrh    r3, [fp, #-10]
        ldr     r2, .L4+8
        cmp     r3, r2
        bhi     .L2
        ldrh    r3, [fp, #-10]  @ movhi
        and     r3, r3, #255
        and     r3, r3, #15
        and     r3, r3, #255
        sub     r3, r3, #4
        strb    r3, [fp, #-5]
.L2:
        ldrh    r2, [fp, #-10]
        ldrb    r3, [fp, #-5]   @ zero_extendqisi2
        mov     r3, r3, asl #12
        add     r3, r2, r3
        ldr     r2, .L4+12
        ldrb    r3, [r2, r3]
        strb    r3, [fp, #-11]
        b       .L3
.L4:
        .word   table
        .word   8179
        .word   8187
        .word   buffer


código em C e em ASM

A área de interesse começa em .L3 e, no pior caso, a função consome 28 instruções. No ARM, normalmente, as instruções de carga (ldr, ldrh e outras) são executadas em 2 ciclos de clock. Assim, num cálculo meia-boca, parece que vai caber nos 42 ciclos de clock que tenho para executar a função.

Três coisas interessantes:
1. Se trocar o tipo da variável "bank" para long (32 bits) o código cai para 26 instruções.
2. Se trocar o tipo do vetor "table" para long (32 bits) o código cai para 24 instruções.
3. Trocando o escopo das variáveis de local para global o código aumenta para 39 instruções.
Isso mostra que, sempre que possível, é interessante usar o tamanho nativo do processador usado. No meu caso o ganho do item 1 (2 instruções) ao custo de 3 bytes de RAM compensa. Já no item 2 o custo é muito alto (+128kB de flash) para o ganho resultante (-2 instruções). Já o item 3 é bem conhecido e esperado. Usar variáveis locais normalmente faz o compilador usar os registradores do processador (mais rápido) e não alocação de memória ou pilha para elas.

Ok, continuando os testes chegou a hora de testar na IDE do ARM que, talvez, será usada no projeto. Como esta IDE (e praticamente tudo que usa ARM) usa o GCC esperei resultados parecidos ou melhores (devido as otimizações ligadas no compilador). Só não esperava tão melhor:


void main(void)
{
   801c4: 4b0b       ldr r3, [pc, #44] ; (801f4)
   801c6: 7819       ldrb r1, [r3, #0]
  for(;;)
  {
    addrs = table_addrs[REG_PIOC_PDSR];
   801c8: 4b0b       ldr r3, [pc, #44] ; (801f8 )
   801ca: 681a       ldr r2, [r3, #0]
   801cc: 4b0b       ldr r3, [pc, #44] ; (801fc )
   801ce: f833 3012  ldrh.w r3, [r3, r2, lsl #1]
    if((addrs >= 0x1ff6) && (addrs <= 0x1ff9))
   801d2: f5a3 52ff  sub.w r2, r3, #8160 ; 0x1fe0
   801d6: 3a16       subs r2, #22
   801d8: 2a03       cmp r2, #3
   801da: d803       bhi.n 801e4 
    {
      bank = (addrs & 0x000f) - 6;
   801dc: f003 010f  and.w r1, r3, #15
   801e0: 3906       subs r1, #6
   801e2: b2c9       uxtb r1, r1
    }
    REG_PIOD_ODSR = buffer[addrs + (bank * 4096)];
   801e4: 4a06       ldr r2, [pc, #24] ; (80200 )
   801e6: eb03 3301  add.w r3, r3, r1, lsl #12
   801ea: 5cd2       ldrb r2, [r2, r3]
   801ec: 4b05       ldr r3, [pc, #20] ; (80204 )
   801ee: 601a       str r2, [r3, #0]
    }
    REG_PIOD_ODSR = buffer[addrs + (bank * 4096)];
  }
}

void main(void)
   801f0: e7ea       b.n 801c8 
   801f2: bf00       nop
   801f4: 200788f1  .word 0x200788f1
   801f8: 400e123c  .word 0x400e123c
   801fc: 00084ad4  .word 0x00084ad4
   80200: 200708f0  .word 0x200708f0
   80204: 400e1438  .word 0x400e1438

00080208 _Z2f4:
    REG_PIOD_ODSR = buffer[addrs + (bank * 4096)];
  }
}

No pior caso, depois de entrar no for e cair na checagem de endereços, são 17 instruções! Isso cabe com folga na janela de tempo. Uma coisinha interessante neste código é a linha 801F2, onde o compilador marotamente incluiu um NOP ali que nunca será executado. Pode parecer desperdício, mas aquele singelo NOP serve para alinhar a memória em endereços de 32 bits para que o ARM não gaste mais ciclos quando acessar o que vier depois. Os caras pensaram até nisso ao criar o compilador. Por isso a briga ASM vs C não tem mais sentido hoje em dia. Um programador sozinho em ASM nunca pensará em todas as possibilidades. Já uma equipe de centenas de pessoas fazendo um compilador fará um serviço muito melhor. 

Bom, agora já tenho razões de sobra para acreditar que a plaquinha irá dar conta do recado. Vou montar e testar e, se tudo der certo, mostrar pra vocês também. Aguardem. ;-)

Nenhum comentário:

Postar um comentário

1. Alguns comentários são moderados automaticamente. Caso isso ocorra pode levar algum tempo até que eu veja e o libere.
2. Comentários fora do assunto do post podem ser apagados.
3. Não, eu não posso consertar os seus aparelhos!