Escolar Documentos
Profissional Documentos
Cultura Documentos
Introdução à COMPUTAÇÃO
04 Junho 2020, 09:44 h
DESCRIÇÃO
Errata:
O video de hoje vai abrir com um tema de videogames, mas vamos descer bem
fundo no cérebro do Nintendinho pra criar um vocabulário que eu vou usar na Parte
2, quando de fato vou mexer num emulador de Nintendo. E esse vocabulário vai
servir não só pra videos futuros meus como pra tudo que você for fazer em
computação. Esta é a base, sem esta base tudo que você tentar aprender de
avançado depois vai ser mais difícil.
Infelizmente é muito difícil empacotar estes temas de uma forma que não seja
maçante. Espero ter conseguido pelo menos interessar vocês no assunto.
Canais:
Se você está cético sobre este episódio porque não tem nenhum interesse por
videogames, não se preocupe, a intenção de hoje é apresentar muitos dos conceitos
fundamentais pra você entender melhor o que é programação. Este vai ser mais um
daqueles episódios que você vai ficar se perguntando "mas onde diabos esse
japonês quer chegar" mas com paciência as peças se encaixam. E já aviso que esse
também vai ser um vídeo bem complexo. Pausa, volta e assiste de novo. Não é um
conteúdo que dá pra entender 100% assistindo só uma vez.
Com tudo isso fora do caminho vamos lá. Quem me acompanha nos Instagram da
vida sabe que um dos meus hobbies favoritos é jogar videogames. Além dos jogos
Triple A eu também gosto bastante de voltar aos jogos da minha infância, de Alex
Kidd a Sonic a Super Mario. Eu não faço coleção de cartuchos porque não teria
espaço físico pra tudo. São milhares e milhares de jogos. Felizmente hoje é muito
simples. Basta baixar uma das dezenas de emuladores e carregar a ROM dos
games. Por exemplo, posso carregar este Retro Arch que tenho no meu desktop e
boom, Super Mario.
A idéia deste video veio quando eu tava assistindo dois dos meus canais de
YouTube favoritos. O primeiro é o Retro Game Mechanics Explained. O cara é
muito bom. Por exemplo, pra quem gosta do Pokemon original de GameBoy, ele
explica bit a bit o mistério do elusivo MISSINGNO, recomendo que assistam. Mas
o video que eu tava assistindo me relembrou de outro dispositivo que existia
durante os anos 90.
Muitos de nós gostamos de games só que mais como passatempo, e não somos tão
obsessivos em completar 100% dos jogos ou fazer speedruns pra bater o recorde de
quem acaba no menor tempo, e só porque um jogo é antigo não quer dizer que é
fácil. Muitos tinham dificuldade maior e mais brutal que jogos da atualidade.
Tentem jogar coisas como o Megaman original ou Battletoads do Nintendinho pra
ver o que quero dizer. Fora que antigamente nem todo jogo tinha uma bateria pra
salvar o progresso, então se dava game over você realmente tinha que recomeçar do
zero.
Enfim, nos anos 90 a tecnologia era mais simples. Existiam diversas marcas
diferentes como Action Replay, Game Shark ou o mais famoso, o Game Genie.
Funcionava assim, você plugava o cartucho na parte de cima e daí plugava o
conjunto no console. O Game Genie ficava entre o cartucho e o console. Ele
engana o console. Por exemplo, o console pergunta pro cartucho, com quantas
vidas que o Super Mario tem que começar? Daí tá gravado em algum lugar no
cartucho o número 3, mas aí se você colocar um código mágico no Game Genie,
como AATOZE, ele devolve 9 pro console no lugar de 3 e o jogo começa com 9
vidas.
Eu vou explicar o game genie em mais detalhes na Parte 2 deste video, de uma
forma complementar que o Retro Game Mechanics explica. Mas antes de chegar
nisso vou fazer uma longa tangente hoje com meu segundo canal favorito, o Ben
Eater. O canal do Ben é hardcore, ele explica todas as fundações eletrônicas da
computação, em particular exatamente a arquitetura por trás de computadores como
o nintendinho. Eu vou usar muito material do canal dele e condensar num único
video pra tentar fazer vocês finalmente enxergarem a mulher de vermelho no
código do Matrix.
(intro)
Agora sim, aquele bloco de bits é o número 48 em hexadecimal, que não é a mesma
coisa que 48 em decimal. Já vou explicar. Essa é a letra "H". Depois temos 6c e 6c
que obviamente é o código hexadecimal pra letra "L". E assim por diante. Quem
disse que 48 é H ou que 6c é L? É uma convenção internacional conhecida como
Unicode. É um conjunto de tabelas e regras que mapeiam cada caracter em todas as
línguas num código de 1 ou mais bytes. Emojis também, cada um tem seu código.
Tudo é número. A tabela mais antiga e mais simples que ainda acabamos usando é a
ASCII.
Pra quem não sabe, o que chamamos de "ROM" em retro gaming são os bits que
vem gravados num cartucho e que extraimos pra dentro de um arquivo, é isso que
um emulador de nintendinho lê no lugar do cartucho de verdade. Com o mesmo
comando xxd mas passando com um pipe pro more pra podermos ver as primeiras
linhas, temos o seguinte. Deixa eu explicar melhor o que é essa representação do
comando xxd. Aliás, existem diversas ferramentas que conseguem abrir binários,
um dos mais usados é o hexdump mas prefiro mostrar o xxd pra dar outra
alternativa. A diferença dos dois é que o hexdump só mostra o binário na forma de
hexadecimal, o xxd faz a mesma coisa mas consegue também gravar de volta pro
binário. Por isso se somar o xxd com o vim você consegue editar qualquer binário
diretamente.
Na 3a coluna ele mostra "." (ponto) quando o byte não tem representação em texto.
Quando tem, ele mostra o que seria o código na tabela ASCII. Vocês vão notar que
nos primeiros 3 bytes temos aparecendo o texto "NES" nessa terceira coluna. NES
é como o Nintendinho é chamado nos Estados Unidos, o Nintendo Entertainment
System. Obviamente não é coincidência, todo cartucho distribuídos nos Estados
Unidos começa com esse código.
Isso é só uma etiqueta arbitrária pra facilitar identificar o que é esse binário. Muitos
outros formatos de arquivo fazem a mesma coisa. Se eu abrir uma imagem formato
PNG com o xxd olha só, aparece um código 89 e depois os códigos ASCII que
forma a palavra PNG. Se abrirmos uma imagem JPEG todos começam com FF D8
FF E0 e alguns bytes pra direita aparece JFIF que é sigla pra JPEG File Interchange
Format. Se listar um binário compilado com extensão .class de Java olha o que
aparece. O identificador de todo binário Java é o hexadecimal que forma as
palavras "cafe babe".
Se não ficou claro, o que estamos vendo na tela com o xxd não é o binário de
verdade, assim como um editor de textos como Notepad mostra uma representação
em forma de texto legível dos números binários reais, o xxd mostra os binários de
uma forma que seja fácil da gente conseguir acompanhar. Essas colunas da
esquerda e direita não existem. E não existe quebra de linha. Um binário são os bits
escritos um atrás do outro numa única linha contínua até o fim. Pense num arquivo
binário como um rolo de fita. Entendem agora porque você vê computadores
antigos gravando dados num rolo de fita? Ou num cassette? É assim mesmo que
funciona até hoje. Se você já programa em alguma coisa, pense num binariozão
com um Array.
Existem vários sites e videos que ensinam em detalhes como converte e faz contas
em binário e hexadecimal então vou resumir hiper resumido. Basicamente sistema
decimal representamos números que vão de 0 a 9. Em hexadecimal representamos
números indo de 0 a F, onde A é 10, B é 11, até F que é 15. Todas as regras
aritméticas de soma, subtração etc funcionam igual. Quando você faz F + 1, coloca
zero e sobe 1, daí o hexadecimal 1 0 equivale a 16. Sim, você precisa se acostumar
a manipular números em hexadecimal. Até mesmo quando trabalha com front-end e
CSS sabe que as cores são mapeadas em números hexadecimais e você faz conta
com elas pra fazer degradê de cores por exemplo, você subtrai pra tornar todas as
cores mais escuras ou faz soma pra elas ficarem mais claras.
Voltando pro cabeçalho do Java, o primeiro "C" não é a letra do alfabeto "C" e sim
o número 12 em hexadecimal. Já brincaram com calculadora de cabeça pra baixo?
Por exemplo, você digita 07734 e vira a calculadora e temos "HELLO" ou
digitamos 376006 e de cabeça pra baixo temos Google. Todo mundo já brincou
disso e o CAFE BABE do java é parecido só que como em hexadecimal temos de
verdade as 6 primeiras letras do alfabeto, dá pra representar palavras. Mas é só uma
brincadeira mesmo.
Entendido isso, vamos pular pra outro conceito. Todo mundo ouve falar sobre
computadores 32 bits ou 64 bits. Se você pensar em base decimal dá impressão que
estamos só dobrando de 32 pra 64. O que isso significa? Em resumo significa que
internamente o computador consegue fazer cálculos e processar números de 32 ou
64 bits de comprimento de uma só vez. Até agora eu fiz exemplos com números de
4 bits que dá pra contar só até 255. Lembra que eu falei que cada 1 bit que eu
adiciono pra esquerda eu estou multiplicando o número todo por 2? Então 5 bits já
é o dobro de 4 bits. 6 bits é o dobro de 5 bits. 33 é o dobro de 32 bits e 64 é 32 bits
ao quadrado. A metade de 64-bits não é 32 e sim 63-bits.
Se abrirmos o nintendinho, esta é a placa mãe com os diferentes chips. Mas por
agora vamos nos focar neste aqui, o MOS 6502 que eu falei antes. Se eu quisesse
mostrar o nível zero de verdade da fundação, precisaria começar com o básico em
eletrônica. Mas eu também só sei o básico. É o que você aprenderia em Engenharia
Elétrica ou Engenharia da Computação e não em Ciências. Mas eu quero resumir
pelo menos, e pra isso resolvi pegar emprestado material de outro dos meus canais
favoritos, do Ben Eater, que eu falei no começo do video.
Uma das playlists dele mostra como você constrói um processador de 8-bits do
zero. E outra playlist dele, por acaso é como fazer um hello world com o 6502, o
mesmo CPU do nintendinho. Só pra mostrar um Hello World ele levou meses
produzindo uns 9 videos com mais de mais hora cada. São horas de video e eu vou
resumir tudo em alguns minutos, então depois eu recomendo assistirem os videos
dele com calma. Como eu disse daria pra descer no nível da física elétrica mas eu
acho que a partir do nível que eu vou mostrar neste video é o mínimo suficiente pra
se ter um desenho na cabeça pra conseguir visualizar o que um programa de fato
está fazendo no hardware. Eu acho muito mais difícil aprender a programar sem
entender o que acontece por baixo.
Quem está iniciando em eletrônica hoje em dia tem bastante material da hora pra
treinar. Uma dessas ferramentas se chama breadboard, que é uma placa feita pra
montar protótipos de circuitos eletrônicos sem precisar ficar soldando os
componentes, você só encaixa os chips e fios na placa, liga a força, normalmente de
5V e pronto. Fazer circuitos integrados é literalmente programação de baixo nível.
Ele inclusive vende kits com todos os componentes que você vai ver. Se tiver como
investir, recomendo como bom aprendizado. É como se fosse brincar de Lego, mas
bem mais interessante.
https://www.youtube.com/watch?v=LnzuMJLZRdU (part 1)
Pra começar veja o diagrama do 6502. Todo chip tem essas perninhas ou pinos,
nelas, você liga ou desliga corrente em alguns pinos pra se comunicar com o chip, e
os traços recebem corrente quando o chip quer enviar dados. Numa placa de
verdade com solda e tudo, o que liga um pino de um chip a outro são traços
condutores. Num breadboard se usa fios de metal mesmo pra facilitar. Agora vamos
ver o diagrama de pinos, e a primeira coisa que quero chamar a atenção são esses
pinos que vão de A0 até A15 e os pinos que vão de D0 até D7. Não é coincidência
que são 16 pinos A e 8 pinos D.
Eu disse que o 6502 é um processador de 8-bits então quando você quer ler o
resultado de algum processamento que ele fez, serão valores de 8 bits de
comprimento, e eles vão trafegar por esses pinos D0 até D7. E quando o
processador quer enviar um endereço pra buscar na memória RAM por exemplo,
ele pode mandar um número de até 16-bits, que trafega por esses pinos de A0 até
A15. Digamos que eu quero mandar o número em hexa a9 pelos pinos de dados.
Em binário seria 1 0 1 0 1 0 0 1, e cada um desses bits vai em um pino de D9 a D0.
Cada componente que coloca no Breadboard sempre vai ligar um pino de força e
outro pino em ground ou Terra, é como você passa energia pro componente. Daí ele
vai ligar outros pinos, por exemplo pra habilitar a CPU, outro pra conectar no
clock. Um clock é exatamente o que o nome diz, é um componente com um cristal
de quartzo que oscila, enviando um sinal elétrico numa frequência precisa e
constante. Todo componente que processa alguma coisa no computador precisa
estar ligado no mesmo clock pra tudo funcionar sincronizado. Um computador é
uma grande orquestra de componentes e o clock é o maestro. Muitas instruções do
CPU demoram mais de um clock, mas tudo é alinhado de acordo com a frequência
desse clock. A menor unidade de processamento num computador é esse pulso que
eu vou ficar chamando de clock durante o video.
Continuando ele precisa ligar o pino que indica pro CPU rodar que é o RDY ou
ready, depois o pino que liga com um botão de reset que é o RESB, daí liga o pino
de clock e ligamos a placa do clock com força e terra na placa principal.
Só de fazer isso e ligando a placa principal numa tomada de 5V a CPU já tá
funcionando e fazendo alguma coisa. Uma forma simples de monitorar pinos de
dados é ligar LEDs direto em cada um deles, por exemplo ligando os pinos A0 a A4
em 5 LEDs pra gente ver se tem alguma coisa acontecendo. E de fato tem alguma
coisa, mas bem aleatório.
Primeiro vamos declarar uma lista com quais pinos do Arduino vamos trabalhar, e
vai ser de 22 a 52. Daí fazemos um loop lendo número a número e mandando um
comando pra dizer que vamos ouvir o que vier nesse pino, ou seja, eles são INPUT
Agora fazemos outro loop pela mesma lista pra ir lendo o que vem em cada pino.
Essa função devolve true ou false então quando vier true devolvemos o número 1 e
quando vier false devolvemos 0 e escrevemos na tela Daí escrevemos uma quebra
de linha com print line e repete tudo de novo. Finalmente não podemos esquecer de
declarar a velocidade que vamos trafegar dados que vai se 57600 bauds, não se
preocupe com esse número mágico.
Podemos ligar o clock em qualquer lugar, mas o Ben decidiu ligar no pino 2 do
Arduino e com isso também precisamos configurar o pino 2 como entrada, pra ler o
pulso que vier de lá.
Agora copiamos o mesmo código que lê dos pinos de endereço da CPU pra ler os
pinos de dados da CPU, é o mesmo loop, só muda os números dos pinos.
Então vamos converter de bits pra hexadecimal e imprimir do lado. Pra fazer isso
inicializamos uma variável com número 0, dai pra cada pino que lemos nesse for ,
damos um shift pra esquerda e adicionamos o novo bit no final, faz shift pra
esquerda, adiciona no final, faz shift e adiciona, e é assim que se monta um número
lendo do binário.
Agora vamos usar uma função que é comum em quase todas as linguagens que
serve pra formatar números em strings. Queremos formatar o número em uma
string hexadecimal de 4 bytes pra representar o endereço de 16-bits, e como 2 bytes
o valor de dados.
Os pinos de dados podem tanto receber dados quanto enviar dados e pra isso serve
esse outro pino do lado, pra dizer qual o modo, se é read ou write. Então ligamos
ele no pino 3 do arduino pra sabermos se o CPU está querendo ler ou escrever.
Como fizemos com os outros pinos, declaramos no codigo como 3 que é onde
vamos ligar o pino RW do CPU. Dizemos ao Arduino pra ler desse pino, ou seja,
input. Quando ler do pino, se devolver true escrevemos "r" pra leitura, se devolver
false, escrevemos 'W' pra indicar write.
Pronto, agora iniciamos o clock. No monitor podemos ver que o CPU tá cuspindo
alguma coisa nos pinos de endereço e de dados mas tá difícil de entender o que tá
acontecendo. Isso porque não tem nada enviando dados ao CPU, daí quando ele
tenta ler alguma coisa dos pinos de dados vem lixo, e tenta rodar esse lixo e fica
soltando lixo, por isso parece aleatório.
Agora que temos o CPU ligado e monitorado, vamos enviar o primeiro dado de
verdade pra ele. Lembrem-se, enviar ou receber dados é literalmente aumentar ou
diminuir voltagem nos pinos. Envie 5 volts no pino D0 e o CPU vai ler o bit 1 do
pino D0, corte a voltagem do pino D1 e o CPU vai interpretar como 0 no pino D1.
Então se quisermos mandar um valor constante nos pinos de dados, podemos ligar
resistores. Se quisermos o bit 0 ligamos o resistor no terra, se quisermos o bit 1
ligamos o resistor nos 5 volts da placa. E com isso configuramos o binário 1 1 1 0 1
0 1 0.
Esse clock simples do Ben tem a opção de pausar e manualmente soltar um pulso
de clock de cada vez clicando num botão. Segundo o manual do processador os
primeiros 7 clocks é uma rotina de inicialização do CPU então podemos ignorar.
Ele vai colocar 0xEAEA nos pinos de endereço e a única resposta que vai ter
continua sendo 0xEA, de novo porque é o único dado fixo nos pinos de dados. O
Ben escolheu 0xEA de propósito porque é um opcode válido do 6502, se olharmos
qualquer documentação dessa CPU vamos descobrir que é o opcode NOP ou No
operation, que é literalmente um comando que não faz nada por 2 ciclos.
Até aqui você acabou de aprender alguns dos principais elementos que tem em
qualquer CPU. Primeiro que ele tem pinos pra enviar endereços, segundo que ele
tem pinos pra enviar e receber dados, que ele tem um contador de programa que
aponta pra endereço da próxima instrução do programa. Terceiro que instruções e
dados são tudo binário, e que não existe diferença entre um byte de dados e um
byte de instrução.
Mas só deixar um valor fixo com resistores não é muito interessante. Pra ficar
melhor, precisamos de alguma coisa que consiga enviar uma sequência maior de
bytes, formando um programa.
https://www.youtube.com/watch?v=yl8vPW5hydQ (part 2)
Pra isso o Ben escolheu uma EEPROM do mesmo fabricante desse clone de 6502
que estamos usando, pra facilitar. Vale explicar o que é isso. RAM acho que todo
mundo tem noção do que é, o que chamamos de memória. Podemos escrever e ler
dados da memória e quando o computador desliga o que está na RAM apaga. Daí
temos ROM que é sigla de Read only memory, ou seja é uma memória que só dá
pra ler o que tem dentro e não é possível sobrescrever nem apagar.
Só pra ter um modelo simplificado na cabeça, pense na ROM como se fosse uma
grade. Pense em cada linha dessa grade como um endereço, de 0 até X. E digamos
que cada linha dessa grade tenha 8 cruzamentos. Quando um cruzamento está
aberto, podemos dizer que é o bit 1, quando um cruzamento estiver queimado,
pense queimado mesmo, ou seja que não tem como recuperar e mudar o bit.
Podemos dizer que é o bit 0. O conteúdo de uma ROM é fixo, não tem como
regravar. Você só consegue enviar endereços e ler o que estiver nesses endereços.
Se olhar o diagrama vemos que a pinagem é muito simples, ele tem pinos de IO0 a
IO7 pra devolver dados e pinos de A0 a A14 pra receber endereços. E porque vai só
até A14 e não até A15. Porque ele só tem 32 kbytes que é metade de 64 kbytes.
Com 16-bits conseguimos contar de 0 até 64kbytes, com 15-bits, que é metade
conseguimos mapear os 32 kbytes que tem nessa ROM, por isso não precisa de
mais 1 bit. Tão começando a se acostumar com essa história de bits? Basta associar
bit com um pino num chip desses.
Repetindo, com 16-bits podemos mapear de 0x0000 até 0xFFFF. Prestem atenção
no comprimento dessas palavras. Cada dígito representa 4-bits. Cada 2 dígitos é um
byte. 16-bits é a mesma coisa que 2 bytes, por isso 4 dígitos. Metade de 16-bits é só
1 bit a menos, o que nos dá os endereços de 0x0000 até 0x7FFF. Se o CPU tentar
mandar um endereço de 0x8000 pra cima, o primeiro bit é ignorado e o restante é
igual à primeira metade, então ele devolve a mesma coisa que devolveria com o
endereço de primeiro bit 0. Por exemplo, se ele pedir o endereço 0x8004 é a mesma
coisa que pedir o endereço 0x0004.
Pra fazer isso funcionar queremos responder somente a endereços que comecem
com primeiro bit sendo "1", ou seja, quando o pino A15 por "1". Aliás, se vocês não
perceberam a ordem dos bits é o inverso da numeração dos pinos, acostumem-se
com isso. Além disso, pra ROM ficar habilitado ele precisa receber baixa voltagem
no pino de chip enable ou CE. É como um botão de liga e desliga da ROM, pra ele
responder precisa estar habilitado também.
Isso é simples, colocamos um inversor entre o A15 e CE assim quando A15 estiver
em alta voltagem, ou seja, representando o bit 1 - porque queremos todos os
endereços que começam bom 1 -, o inversor vai reverter e enviar baixa voltagem,
ou 0, pro CE que vai habilitar a ROM. O resto dos pinos podemos ligar direto da
CPU pra ROM um pra um, A14 da CPU pra A14 da ROM, D0 da CPU pro IO0 da
ROM e assim por diante.
Coisas como esse inversor que mencionei pode ser montado com uma porta lógica
chamada NAND. Eu mencionei portas lógicas no começo do episódio de
Supremacia Quântica, mas o canal do Ben tem videos detalhados explicando como
transistores funcionam e como podemos montar portas lógicas como NAND num
breadboard também. Pro protótipo de hoje ele usa um chip pré-pronto com portas
NAND embutidas. Mas entenda que todos os chips que você estão vendo são
montados com transistores. Imagine transistores como peças de lego e chips como
sendo um conjunto de transistores conectados.
Como falei antes, é com a relação de como montamos circuitos com transistores
que nasce a base da tal "lógica". Como conectamos portas pra chegar no resultado
que precisamos. Neste exemplo simples, queremos uma resposta sempre que o pino
A15 estiver em alta, com bit 1. Mas a ROM precisa do pino CS em baixa, ou bit 0
pra ativar. Portanto, colocamos alguma coisa que inverte o sinal entre elas, um
inversor ou porta NAND, e assim vamos completando o quebra cabeça.
Com a ROM conectada e pronta precisamos carregar algum programa nela. Pra isso
o Ben vai usar Python pra criar um arquivo binário. Poderíamos usar o
próprio xxd pra escrever na mão. Mas qualquer linguagem de programação
também consegue fazer a mesma coisa, use o que você achar mais fácil, mas vamos
seguir no código dele que é simples de entender.
Relembrando que antes a CPU recebia somente o valor fixo dos registradores, o
hexadecimal 0xEA que é a instrução ou opcode NOP. Vamos gerar um binário que
devolve exatamente a mesma coisa.
Primeiro declaramos uma variavel que vai ser um array de bytes. O conteúdo vai
ser 0xEA 32 mil 768 vezes que é o total de bytes que dá exatamente 32 kilobytes.
Agora vamos abrir um arquivo pra escrever em modo binário. Com o arquivo
aberto, escrevemos o array de bytes direto e isso vai gerar o arquivo "rom.bin"
Se fizermos um hexdump podemos ver que só tem 0xEA. Quando tem um asterisco
é a ferramenta hexdump querendo dizer que o mesmo conteúdo se repete até o fim,
que é o endereço 7FFF, o último em 32 kilobytes.
Lembrando que a CPU vai pedir endereços a partir de 0x8000 mas como a ROM
não recebe o primeiro bit, só recebe os 15-bits restantes do endereço, então o
endereço 0x8000 vira automaticamente endereço 0, como já expliquei.
Agora podemos pegar o arquivo binário e usar o programa minipro que envia os
bits pro gravador de EEPROM. Isso é só um detalhe relevante se você for usar um
gravador de EEPROM como na demonstração do Ben, mas é assim que se grava, se
você tinha curiosidade.
Finalmente colocamos a EEPROM de volta na placa. E ligamos o Arduino de novo
pra monitorar.
Pra ver passo a passo damos um reset e pausamos o clock. Agora podemos ir passo
a passo. Os primeiros 7 clocks é a inicialização do CPU, depois disso ele lê dos
endereços fffc e fffd e lê 0xea como antes. E com isso ele pula pro endereço 0xeaea
e continua lendo só 0xea. Até aqui replicamos exatamente o que os registradores
estáticos faziam. Vamos fazer algo um pouco mais útil.
Voltamos pro programa em python e depois de ter o array de bytes com 0xea,
acessamos a posição 0x7ffc e colocamos o valor 0 e na posição 0x7ffd colocamos o
valor 0x80. Duas coisas, lembram na explicação de binário que eu mostrei
exemplos de Javascript, que expliquei que podemos acessar posições num array
tanto com números decimais quanto com números direto em binário? Escrever
0x7ffc é a mesma coisa que escrever 32764.
E repetindo, por que 0x7fffc e não 0xfffc que é o que o CPU vai procurar? Porque a
CPU vai mandar o endereço 0xFFFC pra ROM, mas o primeiro bit é ignorado?
Lembra como o endereço 0x8000 vira 0x0000? Então é só fazer 0xFFFC - 0x8000
e voilá temos 0x7FFC. Não entendeu? 0xF em hexa é 15, subtrai 8 e temos 7.
Agora uma coisa que eu deixei pra explicar só agora. O CPU vai pedir o valor do
endereço 0xFFFC e vai encontrar 0x00 e daí vai pedir 0xFFFD e vai encontrar
0x80 e esses dois valores vão formar o endereço 0x8000. Essa forma de ordenar os
bytes "ao contrário" se chama Little endianess. Outros processadores como um
Intel ou AMD ou mesmo os ARM de celulares também são little endian. O
processador IBM PowerPC dos Macs anteriores a 2006 eram Big Endian, então os
bytes iam aparecer na ordem natural que nós humanos lemos. Mesmo os
processadores ARM de hoje são bi-endian, quer dizer que ele suporta ambos mas a
maioria é usado como little endian.
Parece meio estranho, porque eu ia querer ler números de mais de um byte em
ordem reversa? Na realidade parece estranho porque a gente lê da esquerda pra
direita, mas quando implementamos em hardware é mais simples ler da direita pra
esquerda, requer menos ciclos pra fazer contas. Sem entrar em detalhes do
hardware vamos dar um exemplo onde a ordem natural pode não facilitar nossa
vida. Imagine datas no formato brasileiro com dia, mês e ano. Mande um
computador ordenar. Podemos usar o comando sort que tem em todo linux.
Porém coloque as mesmas datas agora em formato japonês, que é ano, mês e dia e
tente ordenar da mesma forma. E veja só, só de mudarmos a ordem dos elementos
das mesmas datas, a ordenação funcionou muito mais fácil.
Aliás isso é um insight que todo programador precisa ter. Boa parte do que é
programação é retrabalhar os dados pra ficar mais fácil de computar o que
queremos. Little endian no nível do processador é algo parecido na hora de fazer
contas. Pra agora só assuma que quando você vê dois bytes que representam
endereço de 16-bits, eles vão estar invertidos e 0x0080 vai ser 0x8000.
Uma vez eu justamente precisei fazer um pequeno protocolo de rede binário pra
trafegar dados entre computadores Intel e PowerPC. Acho que eu estava usando um
Macbook G4 antigo. E eu recebia lixo no lado do Intel. Até que me dei conta "puts,
PowerPC é Big Endian", daí eu inverti os bytes no meu protocolo antes de mandar
e do lado do Intel passou a receber direitinho. Você não vê isso porque se o único
protocolo que já usou foi HTTP na camada de baixo no nível do TCP ele se vira pra
reverter os bytes se trafegar entre processadores com endianess diferentes. Se você
já usou algo como Google Protobufs, a biblioteca se encarrega de converter se
precisar.
Isso tudo explicado, vamos fazer um programinha agora. Lembrando, um CPU tem
registradores e pode guardar dados de 8-bits, ou 1 byte neles. Pense em
registradores como se fossem variáveis globais numa linguagem de programação
qualquer. Elas vão ser uma ordem de grandeza mais rápidas de acessar do que se
tiver que sair do CPU e pedir memória pra um chip de RAM por exemplo.
Registradores que podemos usar no 6502 incluem o A, X, Y e outros. Cada
registrador vai ter um comando de LOAD pra jogar um valor nele e um comando
de STORE pra tirar o valor do registrador e jogar em algum outro lugar na
memória, se tiver RAM por exemplo.
Como agora temos um valor no registrador A podemos fazer alguma coisa com
isso, tipo escrever nos pinos de dados. Pra isso usamos o STORE from A ou Grave
a partir de A, que é o comando STA e ele recebe um argumento de 2 bytes que é o
endereço pra onde queremos enviar o valor de A.
Rodamos o script python pra gerar o novo binário, gravamos na EEPROM com o
minipro de novo, recolocamos a ROM na placa e ligamos a energia e o monitor do
Arduino.
Fazemos reset com clock pausado, e vamos manualmente clock a clock. Mesma
coisa de antes, ignoramos os 7 primeiros clocks que é a inicialização. A CPU pede
os valores de 0xfffc e 0xfffd que puxa da ROM o endereço 0x8000, esse é o reset
vector e a CPU pede o que tem nesse endereço, que sabemos que é o endereço 0 da
ROM e tem a instrução LDA que é 0x9a.
O contador vai incrementando a partir de 0x8000 e pede o que tem em 0x8001 que
é o valor 0x42. A instrução está completa e a CPU grava no registrador. Agora o
contador pula pra 0x8002, e de lá tem a próxima instrução STA que é 0x8d. O CPU
vai incrementar o contador e ler os proximos dois bytes, que vai ser primeiro 0x00
e depois 0x60, esse é o endereço 0x6000.
Tudo funcionando conforme esperamos mas ao configurar os pinos pra gravar, não
tem ninguém no barramento que responde a essas ordens, e nada acontece. No
próximo ciclo esse dado vai sumir do barramento. Precisamos de alguma coisa pra
segurar esse dado enquanto a CPU fica livre pra fazer outras coisas. Esse seria um
componente chamado "Latch" que acho que podemos traduzir pra "Tranca". Pra
esse protótipo o Ben escolheu um chip que foi feito pelo mesmo fabricante desse
clone de 6502 e é um adaptador que serve pra muitas coisas incluindo trancar
alguns valores. Pense como se fosse um mini-memória.
Por acaso, os pinos desse adaptador são muito parecidos com o da ROM que já
estamos usando, e tem espaço pra gravar até 2 dados de 8 bits no que ele chama de
"Portas". Tem uma Porta A de 8-bits e uma Porta-B de 8-bits. Por isso você tem
pinos de PA0 a PA7 e pinos PB0 a PB7. A essa altura vocês já devem ter entendido
a idéia de pinos serem cada um 1 bit né?
Diferente da ROM não precisamos de 16 pinos pra endereço porque esse chip só
consegue armazenar no 2 valores. Sabemos que queremos ativar esse chip quando
passarmos endereços que comecem com 0x60 que em binário é 0110 0000.
Resumindo o que o Ben explica no video dele, ele vai ligar os endereços A15 a A13
com os pinos de chip select que são CS1 e CS2 e colocar um inversor e outra porta
NAND no meio deles pra configurar o chip corretamente. Os detalhes não
importam pra onde eu quero chegar, mas recomendo que assistam o episódio dele
pra entender os detalhes.
Pra ver isso em funcionamento podemos ligar alguns LEDS nos pinos da Porta B.
Assim quando o valor for trancado, ele vai ligar os LEDs correspondentes.
De volta ao python mas antes de continuar, é uma boa hora pra refatorar esse
código pra ficar mais legível. É assim que se programa, primeiro fazemos a coisa
mais simples que funciona primeiro. Agora vemos padrões no código que dificulta
continuar, então pensamos em formas de tornar o código mais fácil de manusear.
Um das formas de fazer isso é definir um novo array de bytes em cima do array
cheio de 0xEA's, esse array vai conter nosso programa exatamente igual antes. É só
colocar os bytes que estávamos posicionamento manualmente embaixo, em
sequência em cima e apagar o que tava embaixo.
Agora o array de ROM vai ser esse novo array de código mais o array preenchido
com 0xea, só que em vez de ser vezes 32768, vai ser 32768 menos o comprimento
do novo array de opcodes. Assim o array do binário vai ter exatamente o mesmo
comprimento de antes e isso é importante porque tem que ser tudo preciso.
Escrever tudo numa linha só ou dividir em multiplas linhas pro python não faz
nenhuma diferença, só faz diferença pra gente conseguir ler mais fácil. E de
proposito colocamos dois bytes numa linha porque o primeiro é o hexa da instrucao
LDA seguido do seu argumento. E na segunda linha colocamos 3 bytes onde o
primeiro é a instrucao STA seguido de 2 bytes que formam o endereço pra onde
gravar o conteudo de A.
Esse código é exatamente igual o que tínhamos antes, mas agora temos a Tranca
que exige primeiro uma instrucao pra dizer a direçao pra saber qual porta usar.
Então precisamos mandar o endereço 0x6002. Mas também precisamos mandar
algum dado pelos pinos de dados. Se mandar tudo 0 ele habilita a porta B pra ser
input e se mandar tudo 1 ele habilita a porta B pra ser output e gravar o que receber.
Enviar tudo 1 em 8 bits é o hexa 0xFF. Então mudamos o nosso array de código pra
em vez de carregar aquele 0x42 vamos carregar com o número 0xFF. E em vez de
mandar esse valor pro endereço 0x6000, vamos mandar pro 0x6002, e com isso
setamos a direção do chip pra porta B e pra ela ser output.
Pra ficar mais legal podemos agora repetir os comandos, e mandar armazenar 0xaa
em hexa que é 1010 1010 em binario, alternando os bits com o valor anterior, dai os
leds apagados vao acender e os acesos vao apagar. Só uma brincadeira pra mostrar
como podemos controlar alguns LEDs.
Pra finalizar seria legal colocar essas duas sequências num loop pros leds ficarem
piscando sem parar, repetindo essas sequências. E a forma de se fazer loop,
segundo a documentação do 6502 é com um jump que é a instrução JMP. Ele
manipula o contador de programa pra apontar pra outro endereço qualquer, fazendo
o CPU continuar incrementado de lá.
Então podemos fazer JMP pra 0x8000 que é o endereço 0 na ROM e começar do
zero, mas não tem necessidade de reiniciar tudo. Um jump pro endereço 0x8005 é
suficiente, ou seja, pulando os primeiros 5 bytes. Agora podemos rodar o script,
gerar o novo binário, gravar na EEPROM, ligar e testar. E pronto, LEDs piscando
como esperado.
https://www.youtube.com/watch?v=oO8_2JJV0B4 (parte 3)
Se você chegou até este ponto e tá entendendo mais ou menos, no mínimo já deve
tá ficando meio ansioso de ver esse monte de bytes num array. Neste ponto o Ben
resolveu explicar uma forma menos feia de gerar esse binário. Obviamente
ninguém escreve código em binário direto assim.
Mas considere por um instante que nos anos 50 pra trás era exatamente assim que
precisava escrever. E a forma de fazer isso era com fios. Sabe os pinos de dados e
endereço que ligamos registradores e agora ligamos a ROM? Antigamente era um
patch panel. Lembra aqueles seriados ou filmes antigos dos anos 50 que mostra
uma telefonista conectando uma ligação colocando ou tirando fios de um painel? É
algo parecido. Configura os bits e manda pro computador, um conjunto de bits de
cada vez.
Se você já leu alguma mini biografia do Bill Gates e do Paul Allen, vai lembrar que
o primeiro computador comercial pra qual eles escreveram Basic foi o Altair 8800
que vinha com o processador Intel 8080 de 8-bits. Isso foi um ano antes de aparecer
o processador Motorola 6800 que foi a base pro 6502 que estou mostrando aqui. Se
você nunca viu um Altair veja esta foto, tá vendo esses switches no painel? A
etiqueta em cima deles tá escrito D0 a D7 e embaixo A0 a A15. Agora você já sabe
pra que serve esses switches.
Você tinha que escrever bit a bit subindo ou descendo esses switches, um a um os
bits de dado e os bits de endereço e depois puxar o switch de "DEPOSIT NEXT"
que tem nesse painel pra registrar na memória e fazer isso instrução a instrução,
que é o equivalente do nosso array de bytes. Se você tá achando tedioso o que
estamos fazendo, imagina escrever um programa inteiro só com esses switches, bit
a bit, e sem errar nenhum bit. Essa é a verdadeira definição de "escovar bits".
Felizmente os computadores caseiros que vieram depois ganharam teclados e
monitor e isso facilitou ordens de grandeza a programar.
De qualquer forma, havia uma opção um nível acima de escrever binários direto, e
é escrever em Assembly. E é assembly com "y" no final, que é escrever usando
mnemônicos como usar a palavra LDA em vez de escrever o binário pra 0xa9.
Em vez de escrever direto 0xa9 0xff podemos escrever LDA # $ F F. O dólar indica
que estamos escrevendo um número hexa e o hash significa que é um valor
imediato e não um endereço. Quando compila sem o hash apesar do mnemonico ser
LDA, temos um tipo de polimorfismo como já expliquei. Se passarmos um valor
com hash vamos escolher o 0xa9, mas sem o hash, FF é um endereço e o opcode é
diferente, o 0xa5.
Continuando em vez de 0x8d 0x00 0x60 podemos escrever STA $6000, ou seja,
também não precisamos nos preocupar com a inversão de little endian como falei
antes e escrever na ordem natural de leitura humana.
Nas linhas seguintes fazemos lda 55, sta 6000, lda 11, sta 6000, jmp 8005
É exatamente a mesma coisa que fizemos no binário. Pra declarar que o início do
programa é o endereço 0x8000 colocamos um indicador no começo do
arquivo .org 8000 que declara a origem. Lembre-se, na ROM o endereço inicial
vai ser 0x00 mas do ponto de vista do CPU esse é o endereço 0x8000.
No final do código colocamos um novo .org fffc que é o primeiro endereço que
o CPU sempre vai pedir pra buscar qual é o próximo endereço onde tem a primeira
instruçao do programa, já repetimos isso várias vezes. E aí colocamos um word
0x8000 que indica o endereço inicial pro CPU buscar. Word é como chamamos um
conjunto de 2 bytes.
Salvamos esses comandos num arquivo texto e agora podemos passar isso pra um
Assembler, com "er" no final, que vai traduzir esse texto em binário. Em inglês
Assembly que termina em "y" significa "montagem", que é a instrução de
montagem, ou o código-fonte. E Assembler que terminar com "er" é o montador,
que vai pegar essa instrução de montagem e montar o binário propriamente dito.
Pra completar os 32k vemos que faltou só 2 bytes no final, pra isso só adicionamos
um novo word no fim com 2 bytes pra completar. Mas agora que estamos
escrevendo em Assembly podemos usar funcionalidades do Assembler Vasm que
melhoram um pouco nossa qualidade de vida de programação em baixo nível. A
maioria dos Assembler implementam macros e outras coisas pra evitar que a gente
tenha que ficar fazendo repetições ou decorando endereços de cabeça.
Por exemplo, lembra que temos um JUMP pra um endereço fixo 0x8005? E se
colocarmos mais instruções antes disso? Toda hora ia precisar ficar atualizando esse
endereço na mão. Em vez disso podemos adicionar um label e fazer o jump pular
pra esse label e deixar o assembler calcular qual é o endereço certo.
Ainda dá pra melhorar o codigo. Podemos deliminar a região de reset com um label
e só pra brincar, vamos mudar o valor 0x55 pra ser 0x50. No loop vamos tirar o que
tinha antes e colocar uma instrução nova que é o ROR ou rotate right. Lembra o
conceito de shift pra direita? Pense que 0x0050 é 0 0 0 0 0 1 0 1 daí quando
der shift right vai virar 1 0 0 0 0 0 1 0 e a cada loop ele vai dar shift e
rotacionar pra direita. Significa que vai ter só dois leds acesos a cada clock e eles
vão se "movimentando" pra direita.
Podemos compilar com o Vawm pra gerar o binário, gravar na EEPROM e fazer o
de sempre, colocar a ROM na placa, ligar força, resetar e ver se funciona. No caso
ele tá rotacionando pra esquerda porque a placa tá de ponta cabeça no video, mas
você entendeu.
https://www.youtube.com/watch?v=FY3zTUaykVo (parte 4)
Com os LEDs fazendo o que esperávamos, sabemos que a trava está funcionando e
gravando o dado na porta certa. Então podemos ir um passo além. Essa trava pode
funcionar como um buffer pra uma tela e o Ben conseguiu uma tela de LCD
simples. Novamente, vamos pular a configuração do hardware mas a parte
importante é que ele recebe 8 pinos de dados como a ROM e o latch, e assim como
o latch ele tem 3 pinos de controle onde podemos ativar configurações.
0xff significa tudo 1 em hexa, podemos deixar mais claro escrevendo o número em
binario, bit a bit. Dizemos pro Assembler que isso é um número binário colocando
% (porcento) no começo e ; (ponto e vírgula) é comentario, que não aparece no
binário final.
Agora os pinos de controle do LCD, pra isso vamos setar só os primeiros 3 bits que
são os que conectamos nos pinos de controle da tela. Agora é uma sequência
específica desse modelo LCD, então não se preocupem tanto com os detalhes dessa
parte.
Pra inicializar a tela começamos mandando 00111000. O primeiro bit seta modo 8-
bits, o segundo seta 2 linhas na tela e o 3o seta fonte de 5 por 8 pixels. Por fim um
STA mandando pra PORTB.
Carregamos zero com LDA e mandamos Porta A com STA pra limpar os pinos de
controle RS, RW e E. RS é o pino que seleciona se estamos mandando instruções
ou dados pra tela, RW se estamos lendo ou escrevendo e E se tá habilitado pra
receber comandos.
Agora queremos habilitar com LDA do valor E e depois desligamos esse enable. É
uma sequencia bem burocrática mesmo, a maioria dos hardware tem alguma
sequência de controle assim mesmo, mas é o que a documentação do LCD diz que
precisa fazer. Mas já tá quase acabando.
Cada nova instrução que queremos mandar precisa copiar toda essa sequencia.
Agora mandamos 0000 1110 . De novo 3 bits, mas agora é pra ligar o display, ligar
o cursor e não fazer blink que é ficar piscando na tela.
Finamente podemos enviar a letra que queremos escrever no LCD. Poderíamos dar
LDA do número em binário ou hexa que representa a letra na tabela ASCII. Mas
como estamos usando um Assembler, ele sabe converter sozinho então podemos
escrever o string "H" entre aspas que ele se vira.
Na sequência, além de só habilitar com a constante "E", tem que mandar junto o bit
de RS. Pra isso usamos o operador bitwise de OR que é uma barra vertical. O que
isso faz? Ele faz um OR bit a bit. De novo, isso é lógica. Na prática o resultado é
mesclar os bits de RS e de E. Como RS é 0010 e E é 1000 o resultado vai ser 1010.
Aqui valeria uma tangente mas como já está comprido só quero deixar um lembrete
que essa é uma técnica muito usada especialmente pra quem mexe com protocolos
de rede. Isso é como trabalhamos Bit Fields, procurem sobre isso no Google. É uma
forma eficiente de enviar várias características de alguma coisa embutido num
único byte em vez de ter um byte separado pra cada valor.
Por fim zeramos o bloco de loop pra quando chegar nele ficar num loop infinito e
não fazer nada. Assim ele termina de escrever na tela e não faz mais nada.
Pra escrever o resto das letras do hello world, por agora vamos fazer do jeito mais
porco e sujo só pra ver o resultado rápido. Ou seja, copy e paste nervoso de toda a
sequência da letra "H" pra cada uma das outras letras. Isso é obviamente péssimo
de se fazer. Pra testar a primeira vez quando você ainda não sabe se vai dar certo
tudo bem, mas obviamente vamos voltar e melhorar isso depois.
Vamos compilar e não só o código Assembly ficou longo, mas o binário reflete
isso. Olha como o binario no hexdump ficou gigantesco com mais de 300 bytes só
pra escrever um mísero hello world. Escrever código longo e sujo além de feio e
ruim de manter, também gera instruções redundantes e aumenta o tamanho do
binário.
Agora podemos transferir pra ROM, ligar na placa e, como esperávamos, temos um
Hello World aparecendo. Se o objetivo for só escrever Hello World do jeito mais
feio do mundo, já acabamos, mas podemos só acrescentar uma última coisa pra
melhorar muito esse código.
https://www.youtube.com/watch?v=xBjQVxVxOxc (parte 5)
Já sabemos que JMP pula pra um endereço fixo. O problema é que uma vez que
você pula, não tem mais como voltar pra trás. Pra ter esse recurso podemos usar o
opcode JSR que é Jump Sub Routine, que pula pra uma subrotina e grava o
endereço atual do contador de programas. Daí, no final da subrotina usamos o
comando RTS que é tipo return num Javascript, que vai pegar o ultimo endereço
gravado pelo JSR e colocar de volta no contador de programas, e com isso o CPU
continua na instrução logo depois do JSR.
Isso faz parte do dia a dia de um programador: ter que escolher quando você
escreve um pouco mais sujo pra ganhar performance e quando o ganho de
performance é tão pequeno que não compensa. Não existe uma receita, só com
experiência você vai saber. Mas via de regra você só toma essa decisão depois de
conseguir testar o código e mensurar se o ganho vale a pena ou não. Nunca ache
que só porque um código rodou a primeira, já tá pronto e nunca mais vai mudar.
Neste exemplo besta já refatoramos o código várias vezes, imagina num código
grande de verdade.
Gravamos na ROM, testamos e .... nada acontece, o programa parece que travou.
Por que? Pra descobrir vamos ligar o monitor de Arduino de novo, resetar e colocar
em clock manual pra ir linha a linha. Vamos acelerar direto pro suspeito mais óbvio
que é o JSR.
Temos o LDA que vai configurar a primeira configuração pra tela. Em seguida
temos o opcode 0x20 que é o JSR.
Estamos vendo no monitor que o CPU tenta ler alguma coisa em 0x0123 e 0x0124.
O lance é que JSR e RTS dependem de algum hardware que responde nesses
endereços pra gravar o endereço de retorno, mas no momento nao temos nenhum
hardware que responde.
Quando chama JSR ele tenta gravar nesses endereços, mas não tem ninguém
esperando isso então o endereço de retorno vai pro limbo. Quando chega no RTS
ele tenta recuperar o endereço, e não tem ninguém pra responder, então acaba tendo
algum lixo nos pinos e o CPU assume que é um endereço e tenta pular pra lá. No
caso veio um lixo 0x8d8d e o CPU vai pra lá mas não tem nada e o programa se
perde.
Esses endereços 0x0123 0x0124 são específicos do processador 6502. Segundo a
documentação, de 0x0100 a 0x01FF temos o Stack, uma pilha. E temos mais um
registrador que ainda não mencionei, chamado SP. Todo CPU tem uma série de
instruções implementadas em hardware como o LDA STA JSR e outros. Instruções
como LDA grava no registrador chamado "A". Até agora só vimos 2, o registrador
A e o PC que é o contador de programa. E o ultimo registrador novo importante pra
hoje é esse SP é o Stack Pointer, que guarda um endereço do topo de uma pilha. O
SP é quem permite a existência do conceito de sub-rotinas e funções.
Stack ou Pilha é uma estrutura de dados. Lembra a tal fundação que eu sempre
falo? Se você estudar estrutura de dados, vai descobrir a Pilha, uma das estruturas
mais básicas e que você aprende logo no começo dessa matéria. Todo iniciante tem
que saber o que é uma pilha e quando usar ela. Em resumo, é uma cadeia de
elementos onde você vai colocando um elemento de cada vez no topo. E toda vez
que pedir pra devolver um elemento, vai removendo do topo.
Essa pilha é um tipo particular chamado LIFO, Last In, First Out, ou seja, quem
entra por último, sai primeiro. A vantagem de uma estrutura dessas é que basta
guardar o endereço do topo. Depois que adiciona um elemento, decrementa o
endereço do topo. Pra puxar da fila, devolve o dado do topo e incrementa o
endereço no registrador SP. Quando falamos topo, não necessariamente começamos
do zero e vamos subindo um a um. No caso do 6502 ele prefere iniciar no meio da
pilha, em 0x0124 e vai subindo até 0 e quando chegar no zero ele rotaciona pro fim
da pilha em 01FF e continua subindo.
Toda vez que você faz um JSR, ele vai acumular um endereço na pilha. Se você
fizer mais que 255 jumps sem fazer um Return, ele vai estourar o tamanho da pilha
e vai começar a sobrescrever o primeiro endereço gravado. É isso que se chama de
transbordar da pilha ou, em inglês, Stack Overflow. Mesmo com uma pilha
pequena dessas é muito difícil fazer 255 jumps sem retorno. Quando isso acontece
normalmente é uma recursão mal feita. Recursão é uma sub-rotina que dá JSR pro
começo dela mesma. É muito fácil um iniciante errar com recursão.
Outra coisa interessante de saber é que registradores são literalmente variáveis
globais. Se você já trabalhou com C ou Javascript já deve ter ouvido falar de quão
tenebroso uma variável global pode ser. Antes de fazer o JSR no nosso código,
setamos o registrador A com um valor usando LDA, mas quando pulamos pra
subrotina, dentro dela modificamos esse registrador A. Se depois que
retornássemos o programa esperasse encontrar o mesmo valor original em A,
teríamos problemas.
Como no nosso caso, depois que retornamos não faz diferença porque vamos
escrever a próxima letra em cima do A mesmo, então tudo bem, mas se
quiséssemos evitar algum efeito colateral num código maior, poderiamos primeiro
empurrar o valor de A pro stack também, usando a instrução de push A que é PHA.
E antes de retornar com RTS podemos puxar o valor de A do topo do stack usando
pull A ou PLA. Por garantia então devíamos usar isso no começo e fim de toda sub-
rotina né? Mais ou menos, esse hardware antigo só tem 255 slots na pilha
lembram? Se você souber que não vai estourar, pode fazer isso. Como eu falei, tudo
é um trade-off. Se eu quiser garantias demais, os recursos podem acabam cedo
demais e podemos acabar ganhando um bug de stack overflow sem querer.
https://www.youtube.com/watch?v=i_wrxBdXTgM (parte 6)
Isso é suficiente pra 16 kilobytes de RAM. Por acaso este chip moderno tem mais
espaço que isso, então vamos desperdiçar um pouco neste protótipo. Pra usar tudo
daria mais trabalho pra criar um mapeador de endereços. Mas lembre-se que um
nintendinho tinha só 2 kilobytes de RAM e 2 kilobytes de memória de video. E no
próximo episódio vou explicar porque não dava pra ter muito mais que isso mesmo.
Existe um pequeno circuito que precisa ser adaptado e vou pular essa explicação,
mas em resumo a forma de conectar a RAM na CPU é ligar o A14 da cpu com o
A14 da RAM cruzando com o output enable e o A15 da cpu de novo através de um
inversor e um nand no chip select. O que precisamos saber é que isso vai habilitar a
RAM quando o endereço começar com 0 0 que cobre os endereços que queremos.
Pra entender porque a ligação em hardware é dessa maneira, veja o video do Ben.
Feito isso, agora temos RAM ligado e configurado respondem nos endereços que
precisamos, então teoricamente já temos uma pilha disponível e com isso o
programa deveria funcionar. Se ligarmos e dermos reset, de fato, agora o hello
world aparece como deveria.
Tudo resolvido, vamos ligar o Arduino pra entender como o programa se comporta.
Fazemos reset e deixamos o clock em manual de novo. Como sempre, vamos ter os
7 clocks pra inicializar a CPU e pulamos rápido até o ponto onde o programa faz o
jump JSR que é o opcode 0x20.
Agora ele salta pra subrotina que tá em 0x8060, como já fazia antes, executa a
sequência pra escrever a letra no LCE e chega na instrução RTS que é o opcode
0x60.
A instrução de retorno vai desempilhar o primeiro byte do topo da pilha, que está
em 0x01fd, e lê 0x11. Daí decrementa o registrador SP e lê 0x80 e isso forma o
endereço 0x8011 que é pra onde o programa tem que voltar. E com isso temos o
suficiente de hardware pra fazer jumps o que garante que podemos programar
praticamente qualquer coisa só com o que temos aqui.
Com essas poucas instruções que vocês viram já dá pra ter o modelo da maioria das
funcionalidades básicas de qualquer linguagem: loops, funções e ifs ou branches
que não cheguei a mostrar mas acho que vocês já conseguem imaginar como seria.
Na série do Ben tem um episódio extra depois que ele configurar o LCD onde ele
troca o clock que está montado num breadboard por um clock de 1 Megahertz de
verdade. Mas quando ele faz isso o programa pára de funcionar. Tudo funciona
quando o clock é bem lento, mas quando o clock fica rápido demais, o CPU envia
os comandos pro LCD rápido demais e o LCD não responde a tempo. Pra evitar
isso precisa adicionar um comando no CPU pra perguntar pro LCD se já pode
mandar a próxima letra.
De qualquer maneira eu queria passar pelos principais pontos dos videos do Ben
pra que vocês pudessem ver como um CPU funciona, como ele se comunica com
outros componentes como memória RAM ou um LCD simples. Como um CPU
carrega um programa e executa. No fim do dia, não importa que linguagem você
está usando, ele vai ser convertido em bytes de instruções parecidas como o que
acabei de mostrar. Vocês viram que só de pular do binário pra escrever em
Assembly já ganhamos algumas conveniências que o Assembler Vasm nos dá.
Quanto mais conveniências uma linguagem adiciona, mais abstrações ele faz e
mais lento ele tende a ser. Como falei antes existe um trade off sempre, sempre
existe um balanço entre ser conveniente e ser rápido, entre ser rápido e ser seguro e
assim por diante. Em outros videos eu vou tentar explicar as diferenças das
diferentes linguagens levando em consideração que vocês assistiram este vídeo.
No próximo video eu vou usar o que mostrei aqui de uma maneira mais prática. Eu
mostrei a idéia do Game Genie no começo do video mas não fiz nada com ele hoje,
é o que vou começar mostrando na parte 2. A parte 2 vai ser bem menos tedioso
que esta parte 1, isso eu garanto, porque agora podemos usar tudo que eu mostrei
aqui na prática em programas de verdade.
Uma coisa parece difícil só porque você nunca viu. É que nem um truque de
mágica. Mas depois que a gente revela o truque, as coisas ficam muito mais claras.
Se você não entendeu o video todo, não se preocupe, eu literalmente resumi o
equivalente a umas 5 horas de aula do canal do Ben Eater aqui. Vou deixar os links
pro canal dele nas descrições. Se quiserem adicionar mais a esta conversa não
deixem de mandar nos comentários abaixo, se curtiram o video mandem um joinha,
assinem o canal e cliquem no sininho pra não perder o próximo episódio e, como
sempre, compartilhem o video pra ajudar o canal. A gente se vê na próxima, até
mais.
Se tem um tema que eu queria fazer desde que abri o canal é apresentar algumas
das fundações da computação, tanto do ponto de vista de história e da evolução,
mas principalmente dos fundamentos básicos que eram vários 60 anos atrás e ainda
são válidos até hoje.
O video de hoje vai abrir com um tema de videogames, mas vamos descer bem
fundo no cérebro do Nintendinho pra criar um vocabulário que eu vou usar na Parte
2, quando de fato vou mexer num emulador de Nintendo. E esse vocabulário vai
servir não só pra videos futuros meus como pra tudo que você for fazer em
computação. Esta é a base, sem esta base tudo que você tentar aprender de
avançado depois vai ser mais difícil.
Infelizmente é muito difícil empacotar estes temas de uma forma que não seja
maçante. Espero ter conseguido pelo menos interessar vocês no assunto.