Curso:Z80SoC:PT

From Retro-CPU.run
Jump to: navigation, search

Contents

Anatomia e Desenvolvimento de Um Microcomputador

Nota: Este é um projeto como parte the um hobby, faz uso dos princípios mais simples e práticos de arquitetura e projeto de computadores, e sua construção se dá por meio de implementação em FPGA.


Você já parou para imaginar o quanto um computar é complexo, e como os diversos componentes interagem entre si de modo a produzir uma máquina capaz de nos proporcionar benefícios como ferramentas de trabalho, ou diversão na forma de jogos por exemplo? Saiba que os princípios fundamentais da engenharia de computadores que foram utilizados nos anos 70 e 80 para criar as nossas tão queridas máquinas de 8 bits não é tão complicado assim, e é esse o desafio que me proponho a explorar numa série de artigos voltados para todos aqueles curiosos que quem entender como se faz um computador, desde as decisões de arquitetura como tamanho da RAM e resolução de vídeo até o adequado mapeamento de memória e portas de controle de periféricos, passando pelo desenho da BIOS e sua programação em linguagem assembly ou C. Interessado? Então vamos deixar bem claro os objetivos desta série de artigos e, ainda mais importante, o que não faz parte dos objetivos.

Objetivos

  • Um projeto didático que visa estudar a anatomia de um computador, entender seu funcionamento e como as diferentes partes se comunicam entre;
  • Como projetar um computador em todos os detalhes, desde a arquitectura, especificação de seus recursos, implementação usando VHDL e desenvolvimento da BIOS;
  • Utilização de kit de desenvolvimento em FPGA, utilizar a linguagem de descrição de hardware VHDL para implementar o computador, e as linguagens C e Assembly para desenvolver o BIOS;
  • Aprender a utilizar kits de desenvolvimento em FPGA mais utilizados nas comunidades de usuários de retrocomputardores: Terasic DE1, Terasic DE2, Xilinx Spartan 3E.


Não é objetivo desta série:

  • Este projeto não é um passo a passo de como fazer um MSX, Spectrum, TRS-80, ou (nome do seu computador preferido aqui). Para isso, procure na Web e vai encontrar projetos prontos para utilizar, e mais adequados se esta for sua intenção;
  • Não desenharemos placas de circuito impresso, nem soldaremos componentes físicos, uma vez que o hardware será implementado usando FPGA;
  • Não vamos criar um computador de uso genérico – o resultado deste projeto será um sistema de 8 bits ideal para experimentação e aprendizagem.


Não importa se você não sabe programar em Assembly ou C. Ao final, algum conhecimento você terá adquirido nesta área.

Não importa se você não sabe especificar hardware usando VHDL. Ao final você terá adquirido no mínimo conhecimento básico para conseguir ler projetos nessa linguagem.

Não importa se você não sabe como fazer programas que façam a ligação do hardware com software. O objetivo deste curso é principalmente mostrar como estes dois componentes se comunicam.

Pronto para começar?

Introdução ao Z80SoC

O Z80SoC surgiu de um súbito interesse por FPGA em 2007. Não me recordo exatamente como FPGA veio ao meu conhecimento, mas as imensas possibilidades desta tecnologia me conquistou de imediato, e parei literalmente tudo que estava fazendo (come exceção de trabalhar) para me dedicar a conhecer esta tecnologia, suas possibilidades e como a utilizar. E obviamente para explorar FPGAs, é necessário conhecer uma linguagem para descrever o hardware que queremos implementar e assim cheguei ao VHDL. Além do VHDL, é possível especificar hardware para FPGA em outras linguagens como o Verilog, mas após investigação e experimentação com essas duas linguagens, optei pela primeira, que utilizaremos nesta série de artigos. Em seguida à descoberta da tecnologia FPGA, foi necessário identificar como fazer uso dessa tecnologia e cheguei aos kits de desenvolvimento.

Z80soc rom1.png

Z80SoC rodando ROM de demonstração

Kits de Desenvolvimento em FPGA

Após muita pesquisa em fóruns, em principalmente aos diversos projetos existentes de retro computadores e consoles para FPGA, cheguei à conclusão que a imensa maioria dos projetos utilizavam principalmente dois Kits de desenvolvimentos facilmente encontrados no mercado:

  1. Terasic DE1, com FPGA da Altera;
  2. Xilinx Spartan 3E, com FPGA da Xilinx.

Com estas duas placas de desenvolvimento eu conseguiria rodar entre outros, estes microcomputadores, consoles e arcades já livremente disponíveis na web:

  1. Amiga
  2. Apple
  3. Atari console
  4. Colecovison
  5. TRS-80, TRS-80 Color Computer 1, 2 e 3
  6. Commodore 64
  7. MSX
  8. PC 8086
  9. ZX Spectrum
  10. Jogos de Arcade (Pac Man, Galaga, Defender, Space Invaders)
  11. … muitos outros!

A conclusão é então muito simples, e aqui vai minha recomendação de compra, por ordem de custo benefício:

  1. Altera / [DE1]
  2. Xilinx / [Spartan-3E]
  3. Altera / [DE2-70]
  4. Altera / [DE2-115]

Se puder comprar somente uma, compre (1). Se puder comprar duas, compre (1) e (2).

Se já tiver bom conhecimento de VHDL ao ponto de conseguir adaptar e modificar projetos existentes, então poderá adquirir os Kits (3) ou (4). Estes dois Kits não devem ser a primeira opção de um iniciante pelo motivo de serem placas de capacidade superiores à DE1, e portanto muito mais caras. E sendo mais caras, será muito menos comum de encontrar projetos prontos para utilizar nestes Kits, de modo que serão sub-utilizados se você não tiver capacidade de adaptar projetos desenvolvidos para outros Kits.

Características do Z80SoC

O Z80SoC é um sistema que possibilita modo texto 80 x 40 (80 colunas e 40 linhas de texto), e pseudo gráficos uma vez que a tabela de caracteres pode ser redefinida logo após o boot (os bits que compões cada caracter são copiados da ROM para a CHARRAM, que é uma RAM e portanto pode ser escrita). Pode-se escrever no vídeo usando endereços de memória e também através de portas. Na Spartan 3E e na Altera DE2 é possível escrever no display LCD de 2 linhas por 32 colunas da mesma forma que se escreve em RAM; Na Spartan 3E é ainda possível ler o estado do botão giratório, e nas Terasic DE1 e DE2 pode-se escrever no display de 7 segmentos. Todas essas operações são executadas via escrita em endereços de memória ou acesso a portas, tanto em Assembly quanto em C. Para utilização de C com o Z80SoC, foi desenvolvido os drivers básicos de Entrada e Saída, que podem ser utilizados pelos programas do usuário para acesso aos periféricos dos Kits (leds, lcd, memórias, teclado, etc.). Para criar este sistemas, foi utilizado a filosofia de um sistema aberto (como os PC XT da década de 80), ou seja, foi criado a especificação do hardware e depois foi-se ao “mercado” em busca de componentes para montar sistemas, sendo que neste caso o “mercado” é a web e os componentes são os códigos que implementam as diversas partes necessárias para se criar um computador. É possível encontrar códigos opensource para todos os componentes utilizados num microcomputador, e os códigos que foram utilizados no Z80SoC estão listados na tabela a seguir.


Códigos opensource utilizados
CPU Z80 T80 obtido em http://opencores.com/project,t80
Controlador de VGA Livro “Rapid Prototyping of Digital Systems“
Controlador de teclado Adaptado a partir de fonte desconhecida
Controlador de LCD Adaptado a partir do core de Rahul Vora, da Universidade de Novo Mexico
Controlador de 7Seg Desenvolvido especificamente para o Z80SoC
Controlador do botão giratório Adaptado a partir do modelo de referência da Xilinx Spartan-3E
Controlador dos Leds, switch e push button Desenvolvido especificamente para o Z80SoC
Controlador de acessos SRAM Desenvolvido especificamente para o Z80SoC
Controlador de acessos CHARRAM Desenvolvido especificamente para o Z80SoC
Controlador de acessos ROM Desenvolvido especificamente para o Z80SoC
BIOS e código da ROM Desenvolvido especificamente para o Z80SoC

Arquitetura do Z80SoC

Nosso computador terá a arquitetura mostrada no esquema a seguir. CTRL designa os componentes 'controlador. O Device CTRL 'e o controlador dos componentes externos LED, 7SEG, Chaves e Switches, etc. Os demais componentes tem uma controladora própria por serem mais complexos.

Z80SoC.png


Evite Problemas: A partir de agora, podemos referenciar as diferentes placas de desenvolvimento FPGA com “plataforma”.

Mas antes de começarmos a construção do computador, é necessário quais os recursos que queremos implementado, a disposição interna e interligação dos diversos componentes. Está uma definição ultra simplista de arquitetura de computadores. Nesta fase inicial, precisamos considerar aspectos como o que precisamos e o que gostaraimos de ter no computador, tais como:

  • qual o microprocessor
  • layout e tamanho da memória
  • resolução do modo texto (linhas x colunas)
  • resolução gráfica
  • numero de cores
  • som, numero de canais
  • periféricos externos (teclado, mouse, saída de vídeo, acesso a disco, K7)

Uma vez definido os recursos necessários ou desejados, é necessário verificar viabilidade do projeto. No caso do Z80SoC, ou de outro computador implementado em FPGA, é necessário considerar capacidade do FPGA, disponibilidades dos componentes em VHDL, desenvolvimento das partes não livremente disponíveis. Os passos para definir as especificações do computador deve consistir em identificar o kit disponível e suas características, fazendo os ajustes necessários na nossa especificação para viabilizar o projeto - decisões de arquitetura. Desde já vale à pena mencionar que os kits citados e utilizados neste projeto tem características e recursos diferenciados, o que irá impactar diretamente nas capacidades que serão implementados no computador. Mas a despeito de qualquer limitação no kit que você tiver disponível, está será uma jornada divertida e desafiadora.

Mapeamento de Memória

Vamos exemplificar uma decisão de arquitetura com relação à memória RAM do Z80SoC nos diferentes kits FPGA. A Terasic DE1 e DE2 tem uma SRAM interna, enquanto a Spartan-3E não tem esse recurso. Desta forma, a memória do Z80SoC foi implementado usando a SRAM nas DE1/DE2, o que possibilitou 40KB de RAM disponível. No caso da S3E, a RAM teve que ser implementada usando um recurso interno denominado BlockRAM, que é limitado, e consequentemente limitou a RAM nesta implementação em 12KB.O mapeamento da memória do computador, consequentemente, ficou diferente nestes. Este foi um exemplo inicial de decisões que são necessárias tomar antes de iniciar uma implementação, de forma a ultrapassar limitações e se chegar a um projeto satisfatório. Outros desafios surgirão, e serão discutidos em detalhes nas seções correspondentes.


Terasic DE1

+-------------------+ 0000h 
|                   | 
|       ROM         | 
|      16 KB        | 
|                   | 3FFFh 
+-------------------+ 4000h 
|    VIDEO RAM      | 
|   4800 Bytes      | 52BFh
+-------------------+ 52C0h 
|    Variáveis do   | 
|      Sistema      | 57FFh
+-------------------+ 5800h 
|      Tabela de    | 
|    caracteres     | 
|     (charram)     | 5FFFh
+-------------------+ 6000h 
|                   | 
|       RAM         | 
|                   | 
|      40 KB        | 
|                   | 
|                   | 
+-------------------+ FFFFh

Terasic DE2

+-------------------+ 0000h 
|                   | 
|       ROM         | 
|      16 KB        | 
|                   | 3FFFh 
+-------------------+ 4000h 
|    VIDEO RAM      | 
|   4800 Bytes      | 52BFh
+-------------------+ 52C0h 
|    Variáveis do   | 
|      Sistema      | 57DFh
+-------------------+ 57E0h 
|   LCD VIDEO RAM   | 
|      2 X 16       | 57FFh
+-------------------+ 5800h 
|      Tabela de    | 
|    caracteres     | 
|     (charram)     | 5FFFh
+-------------------+ 6000h 
|                   | 
|       RAM         | 
|                   | 
|      40 KB        | 
|                   | 
|                   | 
+-------------------+ FFFFh

Spartan-3E

+-------------------+ 0000h 
|                   | 
|       ROM         | 
|      16 KB        | 
|                   | 3FFFh 
+-------------------+ 4000h 
|    VIDEO RAM      | 
|   4800 Bytes      | 52BFh
+-------------------+ 52C0h 
|    Variáveis do   | 
|      Sistema      | 57DFh
+-------------------+ 57E0h 
|   LCD VIDEO RAM   | 
|      2 X 16       | 57FFh
+-------------------+ 5800h 
|      Tabela de    | 
|    caracteres     | 
|     (charram)     | 5FFFh
+-------------------+ 6000h 
|                   | 
|       RAM         | 
|                   | 
|      12 KB        | 
|                   | 
|                   | 
+-------------------+ 8FFFh

Portas de E/S

O controle de periféricos foi implementado através do acesso a portas do Z80. Seguindo o mesmo princípio aplicado ao mapeamento de memória, as portas foram definidas de forma padrão para as diferentes placas suportadas, sendo que algumas portas adicionais foram introduzidas para possibilitar o suporte a recursos exclusivos de cada placa.

Port In/Out Recurso Plataforma
Portas de E/S na CPU
01H Out Leds Verdes (7-0) DE1 / DE2 / S3E
02H Out Leds Vermelhos (7-0) DE1 / DE2
03H Out Leds Vermelhos (15-8) DE2
10H Out HEX0 a HEX1 DE1 / DE2
11H Out HEX2 a HEX3 DE1 / DE2
12H Out HEX4 a HEX5 DE2
13H Out HEX6 a HEX7 DE2
15H Out LCD On/Off DE2 / S3E
20H In SW(7-0) DE1 / DE2 / S3E*
21H In SW(15-8) DE2
30H In KEY(3-0) DE1 / DE2 / S3E
70H In Botão Giratório S3E
80H In PS/2 teclado DE1 / DE2 / S3E
90H Out Video DE1 / DE2 / S3E
91H In/Out Video cursor X DE1 / DE2 / S3E
92H In/Out Video cursor Y DE1 / DE2 / S3E

(*) Na Spartan-3E, somente 4 switches disponíveis.

Variáveis do Sistema

Variáveis do sistema e registradores são atributos inicializados com valores padrão quando o sistema é iniciado. Estes registradores tem diversas funções como identificar a versão do sistema, onde começa e termina a RAM, etc. Estas variáveis e registradores podem ser de leitura, escrita ou ambos.

Endereço R/W Descrição
Variáveis do Sistema
57DFH R Plataforma: 0=DE1 / 1=S3E / 2=DE2
57DEH R Última tecla pressionada
57DCH R LCD VRAM (Início da Memória)
57DAH R RAMTOP (Última posição de memória disponível)
57D8H R RAM Botton (Endereço inicial da RAM)
57D6H R CHARRAM (Endereço inicial)
57D4H R VRAM (Endereço inicial)
57D2H R Endereço para a pilha do Z80 (Valor inicial é o mesmo da RAMTOP)
57D0H R/W Endereço VRAM para escrever próximo caracter
57CFH R/W Coordenada X do cursor VRAM
57CEH R/W Coordenada Y do cursor VRAM
57CDH R/W Périférico para saída
57CCH R Resolução horizontal em modo texto (colunas)
57CBH R Resolução vertical em modo texto (linhas)
57CAH R Número randômico de 8 bits / parte mais significativa
57C9H R Número randômico de 8 bits / parte menos significativa

Preparando o Ambiente de Desenvolvimento FPGA

Neste projeto será utilizado o ISE da Xilinx, e o Quartus II Web Edition da Alterar, ambos software livres para uso.

Download do Software

Faça o download do software para a plataforma que for desenvolver - Altera ou Xilinx. Não há necessidade de descarregar ambos os softwares se não tiver placas de desenvolvimento da Altera e Xilinx.

Altera Quartus II 13.0 e Service Pack 1
Xilinx ISE WebPack 14.7 e Service Pack 3
Evite Problemas: Utilize as versões indicadas. Ao utilizar versões diferentes, poderá ser necessário adaptações ou conversões do projeto ou componentes que foram gerados usando o Gerador de Componentes. Se você é um usuário inexperiente e optar por uma versão diferente, está se submetendo a problemas desnecessários.

Criação do Projeto Z80SoC

Descarregue o arquivo compactado do projeto e expanda de forma a criar esta estrutura:

Z80SoCv0.7.3/
Z80SoCv0.7.3/memoryCores/
Z80SoCv0.7.3/vhdl/
Z80SoCv0.7.3/ROMdata/

Compilar e Carregar o Projeto no FPGA

  • Execute o software de design (ISE / Quartus II) e abra o projeto Z80SoC contido em Z80SoCv0.7.3/
  • Faça a compilação do projeto. Serão gerados arquivos para programar o FPGA:
    • Altera: um arquivo com extensão .sof.
    • Xilinx: um arquivo com com extensão .bit.
  • Programe o FPGA com o bitstream (.sof ou .bit).

Atualizações de Componentes

Alguns componentes foram criados usando o Gerador de Componentes que existe tanto para Altera quanto para Xilinx. Deve-se utilizar o Gerador para efetuar todas as alterações que forem necessárias nestes componentes.

Quando a BIOS for alterada e compilada, o arquivo gerado (ihx ou mif) deverá ser copiado para o diretório ROMdata, o projeto deve ser compilado novamente no ISE ou Quartus, e carregado no FPGA.

Desenvolvendo a BIOS e Programas

Nesta seção será indicado as ferramentas de desenvolvimento necessárias para programar para o Z80SoC.

Desenvolver BIOS para um computador requer uma completa compreensão da especificação do hardware, mapa de memória e das portas de E/S para acessar periféricos.

O conceito da BIOS vem facilitar o desenvolvimento de software de uso genérico, que devem abstrair o acesso ao hardware e concentrar nas funções necessárias ao programa. A BIOS portanto deve conter todo o código de acesso ao hardware, e prover para os demais programas, um interface de fácil utilização escondendo sua complexidade interna.

Evite Problemas: Utilize os compiladores indicados na próxima seção, a menos que seja experiente e possa facilmente adaptar os códigos desse curso a outros compiladores.

Ferramentas de Programação: O Compilador Assembly

Para programar em assembly, vamos utilizar o Pasmo um compilador cruzado e multi plataforma de assembly Z80. Pasmo permite compilar código assembly Z80 em Linux ou Mac OS X. Entretanto pode ser que você precise compilar o Pasmo a partir de do código fonte.

Para fazer download do Pasmo, siga esse link: http://pasmo.speccy.org. A documentação encontra-se aqui.

Ferramentas de Programação: O Compilador C

SDCC significa Small Device C Compiler, e foi criado para desenvolver código em C compacto e eficiente para micro controladores e processadores de 8 bits como o Z80. SDCC dispõe de extensões para acessar portas de E/S facilmente, que estaremos explorando neste projeto.

SDCC pode ser encontrado aqui: http://sdcc.sourceforge.net

Programando em Assembly

A diversão começa agora.

BIOS do Z80SoC em Assembly

Nesta seção estaremos descrevendo o código em assembly de várias rotinas de acesso ao hardware do computador. Começaremos pelos componentes mais simples e avançaremos para os mais complexos.

LEDs

Cada plataforma dispõe de pelo menos 8 LEDs verdes (DE2 tem nove LEDs, mas o nono é reservado e não ficará disponível para o programador).

Cada LED precisa de um bit para representar seu estado de ligado ou desligado. Bit em um significa que o LED será aceso, em zero o LED será apagado. Para acionar cada um dos LEDs, será utilizado ports de E/S conforme na tabela #Portas_de_E/S. De acordo com esta tabela, os LEDs verdes (7-0) são controlados pela porta 0x01. Os LEDs são numerados da direita para a esquerda. Para acionar os LEDs verdes, é necessário enviar oito bits de dados para a porta 0x01. Cada bit acionará um LED verde.

Alguns exemplos:
Para ligar todos os LEDs verdes, devemos enviar b11111111 (em binário, 0xff in hexadecimal) para a porta 0x01.
Para ligar somente o LED 0 (primeiro à direita), devemos enviar o valor b00000001 para a porta 0x01.

Para controlar os LEDs, será necessário utilizar a instrução OUT do Z80. A seguir ilustramos o código para acionar LEDs verdes, que é parte da biblioteca de sub-rotinas na BIOS do Z80SoC.

LEDG:
    out    (0x01),a
    ret

Quando quisermos aceder um LED, carregamos o valor adequado no registrador A e executamos uma chamada à rotina LEDG. Claro que é pode-se optar por executar o OUT diretamente, sem chamar a rotina LEDG - isto de fato será mais eficiente em termos de processamento e uso de espaço de memória. Estamos definindo as rotinas por razões didáticas e de organização.

Note que o comando OUT vai alterar o estado de oito LEDs de uma vez, o que significa que o programador é responsável por manter o estado dos LEDs em memória, e alterar somente os bits correspondentes ao LED que pretende alterar o estado.

Algumas plataformas tem também um conjunto de LEDs vermelhos. Os LEDs Vermelhos estão disponíveis somente nas DE1 e DE2, e são controlados através da porta 0x02. Referindo-se a #Portas_de_E/S, nota-se ainda que a DE2 tem um conjunto adicional de LEDs vermelhos (18 no total, mas somente 16 ficarão disponíveis para o programador). O segundo conjunto de LEDs da DE2 (15 a 8) são controlados pela porta 0x03.

As sub-rotinas da BIOS que controlam os LEDs vermelhos estão listados a seguir:

LEDR07:
    out     (0x02),a
    ret
LEDR815:
    out     (0x03),a
    ret

Display LED de 7-Segmentos

O Display LED de 7-Segmentos é controlado por pares de segmentos, ou seja, cada duas "letras" ou "números" no display é acionado por uma porta. Os segmentos são numerados da direita para a esquerda. A DE1 tem 4 segmentos, a DE2 tem 8 segmentos e a S3E não tem display LED, portanto esta seção não se aplica à Spartan-3E.

Exemplo de como controlado o 7-Seg Display. Para apresentar o valor "ABCD" no display, devemos enviar os dados para as portas 0x10 e 0x11:

ld    a,0xCD
out   (0x10),a
ld    a,0xAB
out   (0x11),a

A DE2 tem quatro displays adicionais, que serão acionados pelas portas 0x12 e 0x13.

As sub-rotinas na BIOS são estas:

Hex0:
    out     (0x10),a
    ret
Hex1:
    out     (0x11),a
    ret
Hex2:
    out     (0x12),a
    ret
Hex3:
    out     (0x13),a
    ret

Algumas vezes, desejamos mostrar um valor de 16 bits no display. Para facilicar o processo, podemos utilizar estas duas rotinas:

HEXDATA:
;; hl = contêm o valor a mostrar no display
    push    bc
    ld      c,0x10
    out     (c),l
    inc     c
    out     (c),h
    pop     bc
    ret
HEXADDR:
;; hl = contêm o valor a mostrar no display
    push    bc
    ld      c,0x12
    out     (c),l
    inc     c
    out     (c),h
    pop     bc
    ret
Evite Problemas: Note que as rotinas HEXDATA e HEXADDR salvam o par de registradores BC na pilha antes de alterar o registrador C (não é possível salvar somente C na pilha). Esta é uma boa prática de programação que possivelmente evitará diversos problemas como comportamentos imprevisíveis do programam devido a valores de registradores importantes sendo sobre-escritos pelas sub-rotinas.

Push Buttons

Todas as plataformas dispõem de quatro push buttons. A Spartan-3E tem um push button extra: o botão giratório. Neste projeto, o botão giratório da S3E será o quinto push button, e todos eles serão acessados pela porta 0x30.

Para detectar se um botão está pressionado, deve-se ler a porta 0x030. Os bits do valor lido representam o estado do botão (bit em 1, botão pressionado; bit em zero, botão não pressionado). Os bits devem ser lidos da direita para a esquerda, e o quinto bit representa o estado do push button do botão giratório.

PUSHBUTTON:
    in      a,(0x30)
    ret

LCD

O LCD da DE2 e da S3E consiste em um display com duas linhas de 16 caracteres. Este caracteres estão mapeados como endereços de memória que começa em 0x57E0 e termina em 0x57FF (consulte #Mapeamento_de_Memória). Para escrever no display de LCD, tudo que é preciso fazer é escrever os caracteres ASCII nestes endereços de memória, com instruções LD do Z80.

ld    a,0x41          ;carrega letra "A" no registrador a
ld    (0x57E0),a  ;mostra o caracter no LCD, na primeira coluna da primeira linha

Implementaremos na BIOS uma rotina para imprimir uma sequência de 32 caracteres no LCD, de modo que facilite o processo de escrever frases inteiras neste display:

LCD_print:
    ; hl = address of 32 characteres string to print
    push    de
    push    bc
    ; get the LCD start address
    ld      de,(0x57DC)
    ld      bc,32
    ldir
    pop     bc
    pop     de
    ret

Esta sub-rotina da BIOS deve ser utilizado conforme exemplificado a seguir:

    ld    hl,bootmsg
    call  LCD_print
bootmsg:
    ; mensagem a imprimir deve ter 32 caracteres de comprimento
    db    "Booting...      "
    db    "                "

Botão Giratório

O Botão Giratório está disponível nomente na Spartan-3E. Certamente é uma adição interessante a esta plataforma, que poderá vir a proporcionar mais diversão a alguns softwares, como jogos.

Este componente, entretanto, é mais complexo e necessita de um controlador. Foi utilizado um controlador de referência da Xilinx no projeto. A interface de acesso ao botão giratório é muito simples, e basicamente consiste em ler o valor na porta 0x70, que pode ter um de três valores:

  • b00 (ou zero em decimal): Botão não está girando.
  • b01 (ou 1 em decimal): Botão está girando para a direita.
  • b10 (ou2 em decimal): Botão está girando para a esquerda.

A seguir mostramos a rotina para ler o botão giratório, mas lembre-se que pode ler o valor diretamente sem chamar esta rotina, e economizar algum processamento. Para ler o botão, basta ler a porta 0x70 com a instrução IN do Z80.

READ_ROTARY:
    in    a,(0x70)
    ret

ROM de Teste do Z80SoC em Assembly Z80

   org    0x0000
; set stack
   ld      hl,(0x57DA)
   ld      sp,hl
;; read switches to identify tests to run
readsw:
   call    lcdOnOff
   call    SW07
   bit     0,a
   call    nz,sramtest
   call    SW07
   bit     1,a
   call    nz,vramtest
   call    SW07
   bit     2,a
   call    nz,lcdtest
   call    SW07
   bit     4,a
   call    nz,charramtest
   ; loop forever
   jr      readsw
sramtest:
   ; get RAMTOP
   LD      HL,(0x57DA)
   ; save 0x100 bytes for stack before starting test
   LD      BC,0x100
   SBC     HL,BC
   LD      D,H
   LD      E,L
   ; get start of memory (RAMBOTTOM)
   LD      HL,(0x57D8)
   CALL    testmemory
   ret
vramtest:
   call    cls
   ld      hl,(0x57D4)
   call    HEXADDR
   ld      bc,420
   add     hl,bc
   ld      (0x57D4),hl
   call    lcdtest1
   call    HEXDATA
vramtestwait:
   call    SW07
   bit	    7,a
   jr      z, vramtestwait
   ld      hl,0
   ld      hl,(0x57D4)
   call    HEXADDR
   ld      bc,420
   add     hl,bc
   ld      (0x57D4),hl
   call    lcdtest1
   call    HEXDATA
vramtestwait2:
   call    SW07
   bit	    6,a
   jr      z, vramtestwait2
   ret
; test lcdram
lcdtest:
   ld      hl,(0x57DC)
   call    HEXADDR
lcdtest1:
   ld      d,h
   ld      e,l
   ld      hl,lcdstring1
   ld      bc,32
   ldir
   call    HEXDATA
   ret
lcdstring1:
   db "     RONIVON    "
   db "  CANDIDO COSTA "
lcdOnOff:
   call    SW07
   bit     3,a
   jr      z,lcdOn
lcdOff:
   xor     a
   out     (0x15),a
   ret
lcdOn:
   ld      a,1
   out     (0x15),a
   ret
charramtest:
   call    cls
   ; redefine char 0 in the charram
   ld      hl,mychar
   ld      de,(0x57D6)
   ld      bc,8
   ldir
   ;load A with char code 0 ascii
   xor     a
   ; get vram address
   ld      hl,(0x57D4)
   ; move cursor to midle of screen
   ld      bc,420
   add     hl,bc
   ; write char on screen
   ld      (hl),a
   call    HEXADDR
   ret
mychar:
   db  %11111111
   db  %10000001
   db  %10000001
   db  %10010001
   db  %10010001
   db  %10000001
   db  %10000001
   db  %11111111
testmemory:
; Show memory address in 7SEG display left
   CALL    HEXADDR
   PUSH    HL
   LD      HL,0
   CALL    HEXDATA
   POP     HL
;save current value in memory
   LD      B,(HL)
   LD      A,0XAA
   LD      (HL),A
   CP      (HL)
   JR      NZ,memerror
; restore original value to memory
   LD      (HL),B
; prepare to read next memory address
   INC     HL
   PUSH    HL
   SBC     HL,DE
   POP     HL
;; end memory test if next address is equals to end memory address to test
   JR      NZ,testmemory
   RET
memerror:
   ld      hl,0x7777
   call    HEXDATA
   ret
;; ----------- LIB ---------
;; Add this lib to the bottom of your programs
Hex0:
   out     (0x10),a
   ret
Hex1:
   out     (0x11),a
   ret
Hex2:
   out     (0x12),a
   ret
Hex3:
   out     (0x13),a
   ret
HEXDATA:
;;          de = data to print on HEX display
   push    bc
   ld      c,0x10
   out     (c),l
   inc     c
   out     (c),h
   pop     bc
   ret
HEXADDR:
;;          hl = Address to print on HEX display
   push    bc
   ld      c,0x12
   out     (c),l
   inc     c
   out     (c),h
   pop     bc
   ret
LEDG:
   out     (0x01),a
   ret
LEDR07:
   out     (0x02),a
   ret
LEDR815:
   out     (0x03),a
   ret 
SW07:
   in      a,(0x20)
   ret
SW815:
   in      a,(0x21)
   ret
KEY04:
   in      a,(0x30)
   ret
KBD:
   in      a,(0x80)
   ret
cls:
   ld		hl,(0x57D4)
   ld		bc,4799
cls1:
   ld      a,32
   ld		(hl),a
   inc     hl
   dec     bc
   ld      a,b
   or      c
   jr      nz,cls1
   ret

Programando em C

Programar em C pode ser mais produtivo do que programar diretamente em assembly, ou pode ainda ser a única possibilidade quando não se está disposto a abraçar o "lado negro" do desenvolvimento de software. O problema em utilizar linguagens de mais alto nível para computadores de 8 bits é que normalmente o código gerado é muito extenso pois inclui diversas bibliotecas que muitas vezes não são necessárias em todas as aplicações. Outro fator negativo é que o código gerado normalmente é mais lento do que se fosse desenvolvido diretamente em assembly.

Felizmente o conceito de open source viabiliza projetos muito interessantes, e um desses é o compilador cruzado SDCC (Small Device C Compiler).

Com o SDCC é possível escrever programas para diversos processadores (como o Z80) e micro controladores sentado no conforto de um poderoso PC moderno rodando Linux, Mac OS X ou Windows. O código compilador pode ser então carregado no computador de destino.

O SDCC é um compilador C especializado em plataformas de 8 bits e micro controladores, o que significa que o código é otimizado para essas plataformas produzindo um código compacto e veloz, além de outras facilidades como extensões para acesso a portas e acesso direto a memória.

Z80SoC BIOS Nesta seção estaremos descrevendo o código em C de várias rotinas de acesso ao hardware do computador. Começaremos pelos componentes mais simples e avançaremos para os mais complexos. Mas antes, é necessário falar do módulo crt0.s necessário para que possamos utilizar algumas funções padrão da linguagem C.

Módulo crt0.s

Se quisermos utilizar a função printf padrão do C, temos que criar e disponibiliza-la nós mesmo. Isto ocorre porque o printf é uma função de alto nível que não sabe como interagir com o hardware (mais precisamente, o video) de modo a mostrar os caracteres na tela. O printf (incluído na biblioteca stdio.h) pressupõe que existe uma função "putchar" que deverá ser chamada sempre que for necessário apresentar um caracter no vídeo. O módulo crt0.s deve ter essa função definida. Inicialmente, será criado um módulo crt0.s muito simples, com funções básicas minimamente necessárias para ter um sistema funcional. Analise o código a seguir e as descrições para entender como o sistema é iniciado.

Mas agora não era pra falar de C?" 
Sim, mas o C precisa de algumas funções básicas que é mais simples de fazer diretamente em assembly. O módulo crt0.s faz a comunicação de mais baixo nível com o hardware durante a inicialização do sistema, sendo portanto muito razoável que seja implementado em assembly.

         .module crt0
         .globl  _main
         .area	  _HEADER (ABS)
         .org    #0
init:
;; Read the Z80 Stack address
         ld      sp,(#0x57D2)
;; Initialise global variables
         call    gsinit
         call	  _main
         jp	  _exit
         .area   _CODE
         .area   _CABS
;; Ordering of segments for the linker.
         .area	  _HOME
         .area	  _CODE
         .area   _GSINIT
         .area   _GSFINAL
         .area   _DATA
         .area   _BSEG
         .area   _BSS
         .area   _HEAP
         .area   _CODE
; -----------------------------
; putchar
; -----------------------------
_putchar_start::
_putchar::
_putchar_rr_s:: 
         ld      hl,#2
         add     hl,sp
         ld      a,(hl)
_putchar_print::
         ld      hl,(0x57D0)
         ld      (hl),a
         inc     hl
         ld      (0x57D0),hl
         ret
_putchar_rr_dbs::
         ld      a,e
         jr      _putchar_print
putchar_end::
_exit::
         jr      _exit
         .area   _GSINIT
gsinit::
;; outdevice = 0x00 = video
         xor     a
         ld      (#0x57CD),a
         ld      a,#60                  ; screen lines
         ld      (#0x57CB),a
         ld      a,#80                  ; columns
         ld      (#0x57CC),a
         ld      hl,(#0x57D4)           ; get VRAM address
         ld      (#0x57D0),hl           ;current video memory address to print
         xor     a
         ld      (#0x57CE),a		; video cursor y
         ld      (#0x57CF),a		; video cursos x
         .area   _GSFINAL
         ret

Análise do Módulo crt0.s

.org #0
A bios do Z80SoC é armazenada numa ROM acessada a partir do endereço de memória 0x000. org #0 diz ao compilador para alocar o código a partir deste endereço.
call gsinit
Deve conter código que inicia variáveis e registradores necessários ao correto funcionamento do computador quando ele inicia. Os endereços de memória utilizados nesta rotina estão descritos em #Variáveis_do_Sistema.
call _main
Se procurar, não vai encontrar a rotina _main em crt0.s. A função _main (e este label) não deve se alterado em crt0.s pois é um label especial que será utilizado durante o processo de compilação e ligação com o programa principal em C. Programas em C tem uma função especial denominada main, que é o ponto de entrada ou execução (ou programa principal), e é esta função do C que é chamada pela instrução call  _main.
_putchar
Esta função é necessária para se utilizar o printf do C. Nossa função putchar obtêm o caracter a imprimir (que é armazenado na pilha), obtêm o endereço da VRRAM (0x57D0) onde o próximo caracter deverá ser mostrado, escreve naquele endereço (efetivamente, mostra o caracter no vídeo), incrementa o endereço da tela e salva de volta em 0x57D0. Nesta altura do desenvolvimento, o putchar é muito simples e não sabe nem mesmo como lidar com caracteres de controle como CRLF (nova linha), e fim de tela. Estes recursos podem ser acrescentados a qualquer momento ao módulo crt0.s.
jp _exit'
Se o programa principal (_main) terminar a execução, ele retornará e entrará num loop. Isto evita o processamento de partes do código que não desejamos, e eventuais travamentos. 


Evite Problemas: ld sp,(#0x57D2)
A primeira ação razoável a se fazer ao iniciar o sistema é assegurar que a pilha do Z80 é alocada adequadamente. O Z80SoC tem uma variável interna que contêm o endereço da pilha (definido inicialmente para a posição de memória mais alta disponível). 
Imagine que o registrador SP seja iniciado com o valor zero durante o boot do sistema, e em seguida seja efetuado uma instrução "call  gsinit". O endereço de memória para retorno após término da rotina gsinit é colocado na pilha, mas como a pilha está apontando para ROM, o valor não é realmente salvo. A instrução ret vai obter o endereço de retorno da pilha, e seja lá o que estiver armazenado na ROM apontada pela pilha seja utilizado, o que normalmente causa situações imprevisíveis e travamentos.

Arquivo Include com Portas de E/S

O processo de desenvolvimento de rotinas de acesso ao hardware pode ser facilitado e especificarmos os endereços de portas e variáveis e registradores em constantes o C. Dessa forma podemos referenciar a nomes em vez de endereços. O arquivo include deve conter somente as definições de constantes para o seu hardware. Estes arquivos são "incluídos" durante o processo de compilação dos programas.

Evite Problemas: Crie arquivos include para cada plataforma se estiver desenvolvendo para diversas plataformas. Se estiver desenvolvendo para uma única plataforma, pode criar um único include.

Note que os arquivos já contêm os headers das funções que iremos definir mais à frente. isto é necessário para evitar erros durante a compilação.

A esta altura você já deve ter o SDCC instalado e pronto para utilizar. Crie um diretório para seu projeto, digamos ./z80soc, e em seguinte crie subdiretórios com a seguinte estrutura:

z80soc
z80soc/include
z80soc/testsys

Em seguida, vamos criar o Makefile para nosso projeto.

Compilando Programas

Cada componente pode ser executada individualmente, e "linked" no última passo da compilação, conforme exemplificado a seguir. O programa game_demo.c é composto do módulo crt0.s, que é todo em assembly, e mais três módulos em C, incluindo o próprio programa principal. Uma forma de compilar o projeto todo:

1. sdasz80 -o crt0.rel game_demo/crt0.s
2. sdcc -mz80  -I./include -c -o de2115.rel game_demo/de2115.c 
3. sdcc -mz80  -I./include -c -o z80soc.rel game_demo/z80soc.c 
4. sdcc -mz80 -I./include --code-loc 0x0100 --data-loc 0x0 --no-std-crt0 crt0.rel de2115.rel z80soc.rel game_demo/game_demo.c

Na linha 1, o módulo crt0.s em assembly é compilado com o compilador assembly sdasz80 Nas linhas 2 e três, os módulos C com funções são compilados Na linha 4 o programa principal game_demo.c é compilado e "linked" com os módulos anteriores.

--code-loc determina o endereço de memória a partir de onde o programa em C será alojado. Embora a ROM do Z80SoC comece no endereço zero, os 255 bytes iniciais (de 0x0000 a 0x0100) são reservados para o código em crt0.s. O compilador cuida de montar o código final em assembly seguindo algumas diretivas dentro do crt0.s e do parâmetro --code-loc. --data-loc simplesmente diz par o compilador alojar data (como variáveis) logo após o fim do código do programa.

Evite Problemas: Procure utilizar os comandos acima exatamente como mostrados, na mesma ordem e, principalmente --data-loc sem alteração. Utilzar --data-loc com valores diferentes de zero poderá fazer seu programa travar inesperadamente - a menos que você saiba exatamente para que valores alterar.

Makefile

Um arquivo Makefile facilita o processo de compilar e recompilar os projetos. O makefile abaixo pode ser utilizado para diversos projetos, sendo necessário somente criar um diretório z80soc/projeto e alterar "PROGRAMNAME=projeto" conforme o nome do projeto.

Para compilar um projeto:

make clean;make

O resultado será um arquivo .ihx e outro .hex, que são arquivos com os opcodes Z80 em hexadecimal. O arquivo .hex poderá ser utilizado diretamente pelo Altera Quartus II para pré-carregar a ROM do Z80SoC durante o processo de compilação do hardware. No caso das plataformas com Xilinx, será necessário um passo adicional para converter o .hex em .mif (veremos mais à frente).

Evite Problemas: O makefile deve ser modificado para cada projeto. Além de PROGRAMNAME, os arquivos de Include (_DEPS) devem ser alterados de acordo com a plataforma utilizada.
PROGRAMNAME=testsys
IDIR =./include
CC=sdcc
CFLAGS=-I$(IDIR)
ODIR=.
LDIR=./lib
LIBS=-lm 
_DEPS = de2115.h z80soc.h
DEPS = $(patsubst %,$(IDIR)/%,$(_DEPS))
_OBJ = de2115.rel z80soc.rel
OBJ = $(patsubst %,$(ODIR)/%,$(_OBJ)) $(ODIR)/%.rel: $(PROGRAMNAME)/%.c $(DEPS)
       $(CC) -mz80  $(CFLAGS) -c -o $@ $< $(PROGRAMNAME): $(OBJ)
       sdasz80 -o crt0.rel $(PROGRAMNAME)/crt0.s
       $(CC) -mz80 $(CFLAGS) --code-loc 0x0100 --data-loc 0x0000 --xram-loc 0x6000 --no-std-crt0 crt0.rel $^ $(PROGRAMNAME)/$@.c
       rm *.lst *.sym *.lk *.noi $(ODIR)/rom.hex
       packihx $(PROGRAMNAME).ihx >$(ODIR)/$(PROGRAMNAME).hex
.PHONY: clean 
clean:
       rm -f *.rel *.lst *.sym *.ihx *.asm *.lk *.map *.noi

Arquivo Include para Todas as Plataformas

Crie um arquivo com nome: z80soc.h

Este include é genérico e deve ser utilizado em todas as plataformas, pois somente contêm definições presentes em todas as plataformas. Estas funções serão explicadas mais à frente.

Cada #define especifica um nome de constante e um endereço de Porta de E/S do Z80SoC. Desta forma poderemos acessar as portas pelos seus nomes, em vez de endereços.

Note que as portas definidas nestes arquivos Include estão relacionadas na tabela #Portas_de_E/S.


#define GLEDSPORT	0x01
#define DPSWPORTA	0x20
#define PBUTTPORT	0x30
#define KBDPORT		0x80
#define VOUTPORT	0x90
#define VXPORT		0x91
#define VYPORT		0x92
extern void writeMemory(unsigned int address, unsigned char byte);
extern void writeMemoryInt(unsigned int address, unsigned int value);
extern unsigned char readMemory(unsigned int address);
extern unsigned int readMemoryInt(unsigned int address);
extern void cursorxy(short int x, short int y);
extern unsigned char inkey(void);
extern void putchar(char c);
extern char getchar(void);
extern void greenLeds(unsigned char byte);
extern unsigned char pushButton(void);
extern unsigned char dipSwitchA(void);
extern void cls(void);

Arquivo Include para DE1

Crie um arquivo com nome: de1.h

Este include adiciona portas que somente existe nas DE1.

#define RLEDSPORTA  0x02
#define CHEXLSB0    0x10
#define CHEXMSB0    0x11
extern void redLedsA(unsigned char byte);;
extern void hexlsb0(unsigned char byte);
extern void hexmsb0(unsigned char byte);

Arquivo Include para DE2

Crie um arquivo com nome: de2115.h

Este include adiciona portas que somente existe nas DE2.

#define RLEDSPORTA  0x02
#define RLEDSPORTB  0x03
#define CHEXLSB0    0x10
#define CHEXMSB0    0x11
#define CHEXLSB1    0x12
#define CHEXMSB1    0x13
#define LCDCTLPORT  0x15
#define DPSWPORTB   0x21
extern void redLedsA(unsigned char byte);
extern void redLedsB(unsigned char byte);
extern void hexlsb0(unsigned char byte);
extern void hexmsb0(unsigned char byte);
extern void hexlsb1(unsigned char byte);
extern void hexmsb1(unsigned char byte);
extern void lcdonoff(unsigned char byte);
extern void printlcd(short int pos, char s[]);
extern unsigned char dipSwitchB(void);

Arquivo Include para Spartan-3e

Crie um arquivo com nome: s3e.h

Este include adiciona portas que somente existe nas S3e.

#define ROTARY      0x70 
extern void printlcd(short int pos, char s[]);
extern int rotaryButton(void);

BIOS do Z80SoC em C

LEDs

As funções acima recebem funcionam de forma semelhante à versão em assembly, e utilizam a extensão _sfr do SDCC para endereçar portas do Z80 e enviar o byte para o hardware, que acionará os LEDs correspondentes aos bit ativos (em 1).

Por exemplo, para acionar dois LEDs verdes (somente o primeiro e o oitavo LED):

greenLeds(0x81); 
Nota: 0x81 corresponde a "10000001" em binário. Note que os bits em "1" farão o LED acender.


As funções para acionar os demais leds funcionam da mesma forma:

redLedsA(0xAA);
redLedsB(1);

Display LED de 7-Segmentos

Os displays de 7 segmentos (7seg) são numerados da direta (HEX0) para a esquerda (HEX7). A DE1 tem somente os displays HEX0 a HEX3. Para acionar os 7seg, deve-se utilizar as funções hexlsb<n> e hexmsb<n>, onde <n> varia de 0 a 7 (0 < 3 na DE1).


LSB é a parte menos significate do número hexadecimal que pretendemos mostrar no display. MSB é a parte mais significativa. Por exemplo para mostrar o valor "0xFA15" no segmento mais à esquerda, e "0x00A0" no segment mais à direita:

hexmsb1(0xFA);
hexlsb1(0x15);
hexmsb0(0x00);
hexlsb0(0xA0);

Push Buttons

LCD

Botão Giratório

Acesso à RAM

Estão disponíveis 4 funções para acesso à RAM do sistema:

void writeMemory(unsigned int address, unsigned char byte); 
void writeMemoryInt(unsigned int address, unsigned int value);
unsigned char readMemory(unsigned int address);
unsigned int readMemoryInt(unsigned int address);
  • writeMemory escreve um único byte na RAM.
  • writeMemoryInt escreve um inteiro de na faixa de 0 a 0xFFFF (65535) num endereço de memória. Portanto, esta operação vai escrever em dois endereços de memória consecutivos.
  • readMemory lê um byte na memória
  • readMemoryInt lê dois bytes na memória
Uma aplicação frequente de readMemoryInt é ler as variáveis do Z80SoC que precisam de dois bytes, como por exemplo as variáveis RAMTOP (endereço final da RAM) VRAMBOTTOM (endereço onde começa a memória de vídeo).

O exemplo a seguir obtêm o endereço onde começa a VRAM, e escreve o caracter "A" na tela, na tela:

unsigned int vram_address = readMemoryInt(0x57D4);
writeMemory(vram_address, 65);
O código equivalente em assembly:
ld      hl,(0x57D4)
ld      a,65
ld      (hl),a

Programa de Teste do Z80SoC em C

Um programa completo para teste do periféricos do Z80SoC está disponível na área de arquivos: testsys.c. TestSys.c é um código que visa demonstrar como acessar todos os periféricos de uma determinada plataforma. É um código único que identifica a plataforma através da variável 0x57DF e disponibiliza testes para específicos. O uso da biblioteca de funções é exercitado de forma que possa ser utilizado como referência durante desenvolvimento de outros programas.

Biblioteca de Funções em C

Aqui descrevemos algumas funções importantes para o projeto, e também para facilitar o entendimento de partes do código que de outra forma levariam muito tempo de pesquisa para ser compreendido.

Acesso a Portas do Z80 em C

O compilador SDCC dispõe de algumas extensões para facilitar interação com o processador Z80, sendo _sfr uma delas. As funções de acesso a portas em C foram definidas como uso desta extensão, conforme mostrado a seguir. Sugere-se que utilize esa função como template para desenvolver outras funções de acesso a portas.

Nota que o #define deve estar em um arquivo de include à parte conforme visto em #Arquivo_Include_para_Todas_as_Plataformas.

#define GLEDSPORT	0x01
void redLedsA(unsigned char byte) {
       __sfr __at RLEDSPORTA static RLEDIOPort;
       RLEDIOPort = byte;
}


Evite Problemas Note que todas as funções de acesso às portas do Z80 são definidas da mesma forma. Somente o endereço da porta é alterado. Utilize esta função como template. Será necessário alterar o nome da constante no #define e o endereço da porta (e o nome da função, obviamente).

Funções em Assembly dentro do C

O SDCC permite utilizar assembly "inline", ou seja, dentro do código fonte C, utilizando a diretiva "__asm". E também é possível definir funções em assembly, e chamar a partir do programa como se fosse uma função escrita em C.

A seguir serão exemplificados estes dois conceitos através da função writeMemoryInt, que escreve dois bytes na memória (um endereço de 0 a 0xFFFF por exemplo. Esta função faz parte da biblioteca z80soc.h.

1 void writeMemoryInt(unsigned int address, unsigned int value)
2 {
3   address;
4   value;
5   __asm
6   pop     de
7   pop     hl
8   pop     bc
9   push    bc
10  push    hl
11  push    de
12  ld      (hl),c
13  inc     hl
14  ld      (hl),b
15  __endasm;
16 }

1: A função requer dois parâmetros, ambos inteiros sem sinal (porque queremos endereçar memória de 0000 a 0xFFFF, e armazenar lá também valores de 0000 a 0xFFFF).

3 e 4: Estas duas linhas tem como única função "mencionar" as variáveis no cabeçalho da função. Sem essas duas entradas, a compilação emitiria dois warnings pelo fato dos dois parâmetros serem declarados mas não serem utilizados. O programa funcionaria do mesmo jeito sem essas duas linhas.

5: __asm é a diretiva do SDCC para indicar que começa, a seguir, código em assembly.

6, 7 e 8: Ao chamar funções, o SDCC coloca na pilha do Z80 o conteúdo dos parâmetros que foram passados. Sendo assim, para ter acesso aos parâmetros, retiramos-os da pilha com pop.

9, 10 e 11: E fundamental que as rotinas em assembly não interfiram com o estado da pilha, por isso os valores são retornados com push.

12: O registrador HL contêm o endereço de memória (address) e BC contêm o valor a armazenar lá (value). Note que address e value são os parâmetros da função. A primeira parte (LSB) do número inteiro é armazenado na memória.

13: Atualização do endereço de memória para receber a segunda parte do número inteiro.

14: A segunda parte do número inteiro (MSB) é armazenado na memória.

15: A diretiva __endasm indica ao compilador que o código em assembly inline termina aqui.

Note que a posição dos parâmetros da função, passados via pilha do Z80, depende do tipo de dado nos parâmetros. Analise a função "writeMemory" da biblioteca z80soc.c, que armazena bytes (8 bits) na memória para entender como o SDCC lida com tipo char em funções.

Atualização da ROM com nova BIOS

Nesta versão do sistema, a ROM somente pode ser atualizada recompilando o projeto do hardware. Para isso é necessário copiar a nova ROM (rom.hex) para o diretório ROMdata do projeto de hardware, e recompilar. Em seguida, será necessário reprogramar o FPGA com o novo bit stream.

Descrição do Hardware do Z80SoC em VHDL

Aqui começa a aventura mais emocionante da jornada: a arte de especificar hardware usando VHDL.

Espera-se que todos aprendam um pouco de VHDL com este módulo, e para isso o conteúdo será minuncioso e vai incluir muitos detalhes da especificação do Z80SoC. Conceitos importantes serão abordados à medida que forem aparecendo na especificação de cada componente, e por isso a parte inicial será mais longa e detalhada. Uma vez que os conceitos forem sendo apresentados e explicados, não será necessário repeti-los nas seções seguintes quando forem novamente utilizados.

Por isso é de suma importância que as seções iniciais (LEDs, por exemplo) sejam minuciosamente estudadas e compreendidas.

Programando em VHDL

O título é intencionalmente mal formulado, e embora transmita a idéia de um processo que envolve desenvolvimento , não corresponde ao que acontece quando criamos algo em VHDL.

O termo correto seria "especificar um hardware em VHDL"... fazer a especificação (ou design, ou esquema) de um computador usando VHDL. Para os apressados, "programar" é um termo compreensível, mas não é justo na tarefa que realmente se está a executar, pois não "programamos" um esquema ou projeto de computador... nós fazemos o design, a especificação, o projeto. Programar seria correto se estivéssemos falando de software e emulação. Desenvolver um computador em VHDL para um FPGA resulta numa especificação de um computador implementado em FPGA, e não numa emulação. Implementar hardware em FPGA não é o mesmo que emulação.

Outro ponto importante a ter em consideração é que todas as instruções e atribuições de sinais (assignments) ocorrem em paralelo, tudo ao mesmo tempo dentro do FPGA (a menos que se implemente lógica sequencial, que veremos mais à frente). É algo semelhante ao que ocorre num circuito elétrico quando o ligamos a uma corrente: a eletricidade flui e alimenta todos os componentes ao mesmo tempo, a menos que seja controlada ou limitada por componentes e chaves.

No FPGA ocorre o mesmo: tudo acontece em paralelo e ao mesmo tempo. Por isso coisas interessantes podem ocorrer como por exemplo nas seguintes atribuições de sinais A e B:

A <= B;
B <= A;

Como as duas atribuições ocorrem em paralelo, acabam por trocar de valor entre si.

Se as duas atribuições acima fossem comandos da linguagem C por exemplo, após os dois comandos, A e B teriam o mesmo valor (e o segundo comando seria na verdade desnecessário ou redundante). Em VHDL não é isso que ocorre, conforme visto acima.

É possível implementar lógica sequencial em VHDL (semelhante ao C), conforme veremos logo mais.

Módulo Principal

A entidade principal (entity) está descrito no arquivo z80soc.vhd. Este módulo pode ser entendido como a "caixa" e "placa mãe" do Z80SoC, pois ele faz a conexão de todos os demais componentes do projeto. Analisando a "entity" de um design nos possibilita rapidamente compreender suas capacidades, uma vez que temos a identificação de cada componente que ela faz interface.

As interfaces externas do Z80Soc estão especificadas a seguir. Por razões didáticas, está listado a definição da plataforma DE2-115, pois está contém mais componentes. As demais plataformas poderão ter interfaces de entrada e saída diferentes ou em maior ou menos número.

entity  Z80SOC is
   port(
   CLOCK_50                    : in std_logic;
   KEY                         : in std_logic_vector(3 downto 0);
   SW                          : in std_logic_vector(17 downto 0);
   HEX0                        : out std_logic_vector(6 downto 0);
   HEX1                        : out std_logic_vector(6 downto 0);
   HEX2                        : out std_logic_vector(6 downto 0);
   HEX3                        : out std_logic_vector(6 downto 0);
   HEX4                        : out std_logic_vector(6 downto 0);
   HEX5                        : out std_logic_vector(6 downto 0);
   HEX6                        : out std_logic_vector(6 downto 0);
   HEX7                        : out std_logic_vector(6 downto 0);   
   LEDG                        : out std_logic_vector(8 downto 0);    -- Green LEDs
   LEDR                        : out std_logic_vector(17 downto 0);   -- Red LEDs
   -- SRAM
   SRAM_DQ                     : inout std_logic_vector(15 downto 0); -- Data bus 16 Bits
   SRAM_ADDR                   : out std_logic_vector(SRAM_width - 1 downto 0); -- Address bus 18 Bits
   SRAM_UB_N                   : out std_logic;                       -- High-byte Data Mask 
   SRAM_LB_N                   : out std_logic;                       -- Low-byte Data Mask 
   SRAM_WE_N                   : out std_logic;                       -- Write Enable
   SRAM_CE_N                   : out std_logic;                       -- Chip Enable
   SRAM_OE_N                   : out std_logic;                       -- Output Enable
   -- PS/2 port
   PS2_DAT                     : inout std_logic;                     -- Data
   PS2_CLK                     : inout std_logic;                     -- Clock
   -- VGA output
   VGA_SYNC_N                  : out std_logic;
   VGA_CLK                     : out std_logic;
   VGA_BLANK_N                 : out std_logic;
   VGA_HS                      : out std_logic;                       -- H_SYNC
   VGA_VS                      : out std_logic;                       -- SYNC
   VGA_R                       : out std_logic_vector(7 downto 0);    -- Red[7:0]
   VGA_G                       : out std_logic_vector(7 downto 0);    -- Green[7:0]
   VGA_B                       : out std_logic_vector(7 downto 0);    -- Blue[7:0]
   -- LCD
   LCD_RS                      : out std_logic;
   LCD_EN                      : out std_logic;
   LCD_RW                      : out std_logic;
   LCD_ON                      : out std_logic;
   LCD_BLON                    : out std_logic; -- lcd on de2 do not support this signal
   LCD_DATA                    : inout std_logic_vector(7 downto 0));
end Z80SOC;
  • CLOCK_50 é um sinal de entrada(in), e traz os pulsos gerados por um oscilador externos de 50 Mhz. Este oscilador está localizado na placa mãe do kit FPGA. Todos as plataformas suportas trazem pelo menos um oscilador de 50 Mhz, sendo que a DE1 tem também um oscilador de 27 Mhz e outro de 24 Mhz.
  • KEY é um conjunto de 4 sinais que trazem para o FPGA o estado dos botões de contacto.
  • SW é um conjunto de 18 sinais que trazem o estado das chaves micro switch.
  • HEX são 8 conjunto compostos por 7 sinais cada conjunto. Cada um dos grupos de 7 sinais está ligado a um dos displays de sete segmentos.
  • LEDG são os sinais acionar os LEDs verdes.
  • LEDR são os sinais para acionar os LEDs vermelhos.
  • SRAM_* são os sinais para acesso ao chip de memória SRAM (DE1 e DE2).
  • PS2_DAT e PS2_CLK são os sinais para comunicação com o teclado padrão PS/2.
  • VGA_* são os sinais para acionar o VGA. HS e VS são para sincronismo, e RGB são os sinais para determinar as cores no monitor.
  • LCD_* são os sinais de controle do display LCD (S3E e DE2).

Os sinais especificados na definição na "entity" determina quais são os periféricos do computador. Portanto analisando a definição acima, podemos identificar que o computador terá entrada para teclado, saída VGA, saída para display LCD e de sete segmentos, LEDs, botões, chaves do tipo switch e memória SRAM.

Além destes periféricos, o Z80SoC também tem memória ROM, CharRAM, VRAM e LCD RAM que são implementadas internamente usando recursos do próprio FPGA, e por isso não necessita de descrição de interfaces na entidade. Mas estes componentes adicionais estarão bem definidos e identificados na descrição do hardware.

LEDs (HW)

O acionamento de LEDs é executado quando um programa envia um comando de escrita para a porta 1, 2 ou 3 do Z80 (#LEDs_Verdes e #LEDs_Vermelhos. Em C, deve-se usar as funções greenLeds, redLedsA e redLedsB para acionar os LEDs. Estas funções fazem parte das bibliotecas disponíveis neste projeto.

O acionamento de um LED pelo hardware pode der tão simples quanto isso:

LEDR(0) <= '1';

Entretanto, é necessário uma lógica que determine quando um LED deve ser ligado uma vez que são controlados pelo programa do usuário. A lógica que determina o acionamento dos LEDs pelo Z80SoC está definida dentro de um "process":

1 process(Clk_Z80)
2 variable LEDG_sig    : std_logic_vector(7 downto 0);
3 variable LEDR_sig    : std_logic_vector(15 downto 0);
4 begin       
5 if Clk_Z80'event and Clk_Z80 = '1' then
6     if IORQ_n = '0' and MREQ_n = '1' and Wr_n = '0' then
7         if A(7 downto 0) = x"01" then
8             LEDG_sig := DO_CPU;
9         elsif A(7 downto 0) = x"02" then
10             LEDR_sig(7 downto 0) := DO_CPU;
11        elsif A(7 downto 0) = x"03" then
12            LEDR_sig(15 downto 8) := DO_CPU;
13        end if;
14    end if;
15 end if;
16 LEDR(15 downto 0) <= LEDR_sig;
17 LEDG(7 downto 0)  <= LEDG_sig;
18 end process;

Um processo pode ter uma lista de "gatilhos", que acionam a execução do processo (sensitivity list). O processo para acionar LEDs tem o sinal Clk_Z80 em sua sensitivity list, e isso significa que o processo será acionado (executado) na mesma frequência que o clock do Z80 a 3.57Mhz. Em VHDL existem atribuições do tipo sinal e do tipo variáveis. Atribuições de variáveis só podem ocorrer dentro de um processo - se for utilizado fora do processo vai resultar mum erro de sintaxe na hora de compilar o projeto.

Já falamos sobre sinais e como se comportam num sistema onde tudo ocorre em paralelo. Já as variáveis tem um comportamento semelhante às variáveis de uma linguagem de programação, como por exemplo o C. Ou seja, atribuições de variáveis ocorrem imediatamente, e na sequência que aparecer. Pesquise "VHDL variable vs signal" no google e estude alguns dos textos que encontrar para aprender mais sobre sinais, variáveis e a diferença entre os dois.

Um conceito importante para a definição de nosso processo de controle de LEDS é que as variáveis podem ser utilizados como registradores, ou seja, podem armazenar dados temporariamente e rete-los durante os ciclos de execução do processo. Esta característica é importante porque os LEDs devem reter último estado que foi estabelecido pelo nosso programa, e este estado é armazenado numa variável (registrador).

Linha 2: O registrador LEDG_sig é criado, com capacidade para armazenar 8 bits de informação.

Linha 3: Outro registrador para os LEDs vermelhos é criado, com capacidade para armazenar 16 bits de informação (DE2-115). Algumas plataformas tem menos LEDs, e neste caso o registrador deve ter menos bits de capacidade.

Linha 5: O clock do Z80 (Clk_Z80) está na sensitivity list, o que significa que toda vez que o clock oscilar, o processo sera avaliado (executado). Entretanto, o clock oscila entre '0' e '1', e o processo deve ser executado somente a cada ciclo completo - por isso a condição nesta linha verifica se o clock oscilou para '1' antes de avaliar as demais condições relevantes para acionar os LEDS.

Linha 6: Nesta linha, é avaliado os sinais que estão ligados diretamente ao processador Z80: IORQ_n, MREQ_n, WR_n. O "n" no nome do sinal significa que está ativo quando for '0'. Esta linha avalia se o Z80 está executando uma instrução de escrita (WR_n = 0) em uma porta (IORQ_n = 0) e de forma redundante, ainda verifica que não está sendo feito acesso à memória (MREQ_n = '1'). Os sinais significam: IORQ = Input/Output Request, MREQ_n = Memory Request, WR_n = Write Request).

Linha 7: Aqui verificamos o barramento de endereços do Z80 (sinal A) para obter o endereço da porta para escrita. Porta 0x01 é a porta que controla os LEDs verdes.

Linha 8: Se a porta é mesmo 0x01, então devemos ler o barramento de dados DO_CPU para obter o valor que o Z80 está querendo escrever na porta 0x01, e armazenar no registrador que guarda o estado dos LEDs verdes. O acionamento dos LEDs não ocorre ainda neste momento, mas o estado já está identificado aqui.

As linhas 9 a 13 repetem o mesmo processo para os LEDs vermelhos. Note que os LEDs vermelhos tem um registrador próprio de 16 bits (16 LEDs).

Esta parte do código é toda sequencial, ou seja, ela ocorre na ordem em que aparece, e partes do código é executado somente quando determinadas condições são verdadeiras. O comportamento de um processo é semelhante ao de um programa em C por exemplo.

As linhas de 13 a 15 encerram os respectivos IFs. Note que o código dentro dos IFs podem não ser executados durante vários ciclos do processador - o que aliás é muito comum, pois imagine que um programa envia uma instrução para acender um LED e depois vai executar outras funções não relacionadas com os LEDs. O LED que foi ligado deve permanecer ligado até que o programa envie outro comando para apaga-lo. Nesse intervalo em que os LEDs são "esquecidos" pelo programas, os registradores não são alterados, e portanto mantêm o último estado definido pelo programa.

Nas linhas 16 e 17, o estado dos registradores é atribuído aos sinais que estão ligados aos LEDs. Neste momento, os LEDs são acionados (acesos ou apagados). Note que mesmo que os registradores dos LEDs (variables) não tenham sido processados num determinado ciclo, eles retiveram seu último estado, e ao serem atribuídos ao sinal LEDR ou LEDG, vão manter o LED também no estado anterior (seja aceso ou apagado).

Outro termo para referenciar as variáveis que mantêm seu estado entre ciclos é "latch". Latch pode ocorrer propositalmente (como mostrado acima, quando queremos reter o último estado de uma variável) ou por acidente - e neste caso pode causar problemas ao projeto. Leia um pouco mais sobre "latches" pesquisando "latches in vhdl" no google.

Com isso finalizamos a descrição do nosso controlador de LEDs, ao mesmo tempo que introduzimos conceitos importantes de VHDL, que serão utilizados em diversos componentes do Z80SoC e qualquer outro projeto implementado em FPGA.

Chaves Micro Switches

Chaves Push Button

Display LEDs de 7 Segmentos

ROM

RAM

Vídeo RAM

RAM de Tabela de Caracteres de Vídeo (CharRAM)

Display LCD (Spartan 3E e Terasic DE2-115)

Botão Rotativo (Spartan 3E)

Teclado PS/2

VGA

Conclusão

Futuras Melhorias

Cores

Utilização da Memória Flash nos kits FPGA

Utilização da Memória SDRAM nos kits FPGA

Acesso ao cartão SD

Referências

fpgaforfun http://www.fpga4fun.com