CI064                                   © Roberto André Hexsel, 2014-2019



Três avisos sobre o simulador:

(i) não sei como fazer para terminar a simulação sem que os asserts
emitam as mensagens de erro.  De qualquer forma, o texto em maiúsculas
indica o término normal de uma simulação, ou alguma condição de erro
detectada pelo simulador -- leia a mensagem de erro;

(ii) o código de inicialização do processador é executado a partir do
endereço zero, seguido pelo código de tratamento de excessões (excp_0000,
excp_0100 e excp_0180), interrupções (excp_0200) e "hard reset"
(excp_bfc0).  O "código normal" inicia no endereço x_ENTRY_POINT
para que seja fácil identificar quando o processador executa "programas" ou
"tratadores".  O compilador preenche a ROM com zeros, que são desmontados
para NOPs;

(iii) a instrução WAIT é usada para terminar a simulação e seu argumento
indica o motivo.  Na função _exit(), o wait 0 indica
terminação normal; em excp_handler, os códigos de terminação são
aqueles para as respectivas excessões.  Procure por wait em
handlers/start.s pare ver a causa da excessão.



Antes de mais nada, crie um prompt vazio, copiando a linha abaixo no seu terminal alias prompt:="" Três clicks no botão esquerdo para copiar, um click no meio para colar.
Sobre os diretórios de simulação O simulador deve ser executado no diretório ~/cmips/cMIPS Os programas de teste devem ser compilados no diretório ~/cmips/cMIPS/tests e os "executáveis" prog.bin e data.bin devem ser movidos para ~/cmips/cMIPS Sobre a variável de ambiente PATH A variável PATH deve ser aumentada com o caminho dos scripts necessários para as simulações. Se, por acaso ou acidente, você fechar a Shell em que executava as simulações, o caminho para os scripts deve novamente ser acrescentado à PATH.

Da tarefa

Sua tarefa, que vale 5% da média, é resolver os problemas apontados adiante. Suas respostas devem ser enviadas por e-mail para rhexsel@gmail.com até as 23:59 de amanhã, com as respostas textuais em formatação simples, mais os arquivos texto contendo as soluções que envolvem escrever código. Seus programas serão executados e só receberão crédito caso produzam resultados corretos. Plágio não será tolerado, podendo acarretar um inquérito disciplinar. O trabalho pode ser efetuado em duplas. Os dois nomes são parte da resposta.

Contador e o Relógio de Tempo Real

Este material foi escrito sob a premissa de que você leu as notas de aula sobre Sistemas Operacionais e Interrupções, conforme recomendado. Caveat emptor. Copie o novo repositório com: prompt: cd ; git clone https://github.com/rhexsel/cmips.git Este comando criará uma cópia do repositório em seu ${HOME}. Toda a vez em que uma nova versão for copiada do github, aqueles arquivos do repositório que foram atualizados (ou corrigidos) no repositório serão copiados novamente. ACHTUNG: Se você alterar arquivo(s) em sua cópia local, suas atualizações poderão ser sobrescritas pela versão do repositório. Acrescente ao seu caminho o diretório dos executáveis: prompt: export PATH=$PATH:~/cmips/cMIPS/bin Talvez seja uma boa ideia acrescentar a linha acima ao seu ~/.bashrc. Teste sua instalação, e retorne para ~/cmips/cMIPS prompt: cd tests ; ./doTests.sh && cd -

1- Contador acessado por E/S programada

Relembrando: o contador é um dispositivo com um único registrador de 32 bits. O bit 31 controla a interrupção (b31=1 interrompe se contagem==limite). O bit 30 controla a contagem (b30=0 contagem paralisa). Os bits 29..0 mostram o número de ciclos que decorreu desde a última escrita com b30=1. O valor lido do contador é o estado dos dois bits de controle, e a contagem. Leia o código das funções que acessam o contador a partir da linha com // external counter em include/cMIPSio.c . O arquivo include/cMIPS.h define os endereços dos dispositivos. Nosso primeiro contato com o contador emprega polling, como discutido em sala. tests/extCounter.c é reproduzido abaixo.
1 // Testing the external counter is difficult because it counts clock cycles 2 // rather than instructions -- if the io/instruction latencies change then 3 // the simulation output also changes and comparisons become impossible. 4 5 #include "cMIPS.h" 6 7 8 // convert small integer (i<16) to hexadecimal digit 9 #define i2c(a) ( ((a) < 10) ? ((a)+'0') : (((a)+'a')-10) ) 10 11 12 #define N 6 // must be less than 25 13 #define CNT_VALUE 0x40000040 // set count to 64 cycles 14 15 void main(void) { 16 int i, increased, new, old, newValue; 17 18 newValue = CNT_VALUE; 19 increased = TRUE; 20 21 for (i=1; i <= N; i++) { // repeat N rounds 22 print(i); // print number of round 23 // to_stdout( i2c(i) ); // print number of round 24 // to_stdout('\n'); 25 26 newValue = CNT_VALUE | (i<<3); 27 startCounter(newValue, 0); // num cycles increases with i, no interrupts 28 29 old = 0; 30 31 do { 32 33 if ( (new = readCounter()) > old) { 34 increased = increased & TRUE; 35 old = new; 36 // print(new); // print current count, not for automated tests 37 } else { 38 increased = FALSE; 39 } 40 41 } while ( (readCounter() & 0x3fffffff) < (newValue & 0x3fffffff) ); 42 // are we done yet? 43 44 if (increased) { 45 to_stdout('o'); 46 to_stdout('k'); 47 } else { 48 to_stdout('e'); 49 to_stdout('r'); 50 to_stdout('r'); 51 } 52 53 to_stdout('\n'); 54 } 55 56 }
Compile-o (em tests): prompt: compile.sh -v extCounter.c && mv prog.bin data.bin .. Execute a simulação: prompt: (cd .. ; run.sh) O resultado é mostrado abaixo. Os números são as quatro voltas do laço for mais externo, e os "ok" indicam que a contagem é monotônica.
0 00000001 ok 00000002 ok 00000003 ok 00000004 ok 00000005 ok 00000006 ok
Edite tests/extCounter.c e descomente o print() da linha 36. Compile com -O2 e repita a simulação. A contagem será algo parecido com:
00000001 40000008 40000025 40000042 ok 00000002 40000008 40000025 40000042 ok 00000003 40000008 40000025 40000042 40000058 ok 00000004 40000008 40000025 40000042 4000005f ok 00000005 40000008 40000025 40000042 4000005f ok 00000006 40000008 40000025 40000042 4000005f ok
O primeiro valor de contagem, 40000008 mostra que decorreram oito ciclos entre a programação do contador (linha 27) e a primeira leitura (linha 33). Os valores 40000025 e 40000042 são as leituras nas voltas seguintes do laço interno. O 00000001 é imprimido na linha 22, e o contador é reprogramado na linha 27. Note que na linha 27 o contador é programado com valores maiores a cada volta, o que explica o acréscimo de 0x30 na última volta do laço externo. Por que algumas voltas apresentam contagens idênticas? Recompile extCounter.c com '-O3', que é um nível de otimização maior do que o default que é '-O1'. prompt: compile.sh -O3 -v extCounter.c && mv prog.bin data.bin .. Execute a simulação: prompt: (cd .. ; run.sh) E efetue a primeira parte da tarefa.

2- Contador acessado por interrupção

As coisas ficam algo mais interessantes quando as interrupções entram em cena. O próximo programa computa a quantidade de números primos entre 0 e 100 com o Crivo de Eratóstenes. Enquanto computa os primos, o contador conta tempo de 200 em 200 ciclos do relógio de 50 MHz (T=20ns). Ao final da execução, o programa imprime o número de primos e a quantidade de interrupções que ocorreram durante a execução do programa. O programa abaixo é uma cópia de tests/extCounterInt.c.
1 // Sieve of Eratostenes 2 // Counts number of primes smaller than MAX 3 4 #include "cMIPS.h" 5 6 #define MAX 100 7 #define FALSE (0==1) 8 #define TRUE ~FALSE 9 10 extern _counter_val; 11 12 int p[MAX]; 13 14 void main() { 15 16 int i, k, iter; 17 int num; 18 19 enableInterr(); 20 21 _counter_val = 0; // accumulates number of interrupts 22 23 startCounter(200,TRUE); // counter will interrupt after N cycles 24 25 p[0] = 0; 26 for (i = 1; i < MAX; i++) 27 p[i] = TRUE; 28 i = 2; 29 30 while (i*i <= MAX) { 31 if (p[i] == TRUE) { 32 k = i + i; 33 while (k < MAX) { 34 p[k] = FALSE; 35 k += i; 36 } 37 } 38 i++; 39 } 40 num = 0; 41 42 print(num); // debugging only 43 to_stdout('\n'); 44 45 for (i = 1; i < MAX; i++) { 46 if (p[i] == TRUE) { 47 ++num; 48 print(i); // 1 2 3 5 7 11 13 17 19 23 29 31 ... 49 // 00000001 00000002 00000003 00000005 00000007 0000000b 0000000d 50 // 00000011 00000013 00000017 0000001d 0000001f ... 51 } 52 } 53 54 to_stdout('\n'); 55 print(num); // == x01a 56 to_stdout('\n'); 57 58 if (_counter_val > 10) { // more than 10 interrupts ? 59 to_stdout('o'); 60 to_stdout('k'); 61 } else { 62 to_stdout('e'); 63 to_stdout('r'); 64 to_stdout('r'); 65 } 66 to_stdout('\n'); 67 to_stdout('\n'); 68 } 69
A variável _counter_val é usada para acumular a contagem de interrupções; é através desta variável que os dois processos, o Crivo e o tratador de interrupções, se comunicam. Repare que a variável _counter_val é declarada como extern. Esta variável está definida no tratador da interrupção do relógio, nas linhas 16 e 17 de include/handlers.s. A linha 17 aloca 4 bytes para esta variável (um inteiro) numa área de memória que não é inicializada pelo compilador. .comm é a diretiva para alocação de espaço na seção "common". Quando tests/extCounterInt.o é ligado com include/handlers.o, o endereço de _counter_val é disponibilizado ao objeto produzido a partir do programa em C. Após a inicialização (linhas 21-23 de extCounterInt.c), esta variável é incrementada pelo tratador de interrupções (linhas 45-50 de handlers.s) e é lida em main (linha 58). A linha 21 inicializa _counter_val, que acumula o número de interrupções. A linha 23 dispara o contador, programando-o para contar 200 ciclos de 20ns e então gerar uma interrupção. As linhas 25 a 39 computam os números primos entre 0 e MAX. As linhas 40-42 imprimem zero para indicar que a contagem dos primos se inicia. O laço das linhas 45 a 52 imprime os primos encontrados. A linha 55 imprime a quantidade de primos encontrados (0x1a=26) e as linhas 58-68 imprimem o resultado da contagem interrupções, que deve ser maior que dez. Leia com atenção as primeiras 70 linhas de include/handlers.s, que contém o código do tratador de interrupções do contador. Você deverá modificar o código deste tratador como parte das tarefas desta aula. A sequência de execução deste programa "atravessa" três arquivos:
  1. include/start.s contém a inicialização do ambiente de execução: inicializa vários registradores de controle do processador, a pilha, e salta para main(). Neste arquivo também está o demultiplexador de interrupções, que é o tratador da excessão 0x200 (interrupções), a partir do label _excp_0200:.
  2. include/handlers.s contém os tratadores de interrupções, um para cada dispositivo.
  3. tests/extCounterInt.c que contém o programa main().
Estes três programas são ligados para gerar o executável que é simulado no cMIPS. Os dois primeiros contém o ambiente (SO primitivo) no qual main() executa. Compile extCounterInt.c e execute a simulação: prompt: cd tests ; compile.sh -v extCounterInt.c && mv prog.bin data.bin .. prompt: cd - ; run.sh A saída da simulação é mostrada abaixo.
00000000 # mostrará primos a seguir 00000001 # primeiro primo 00000002 00000003 00000005 00000007 0000000b 0000000d 00000011 00000013 00000017 0000001d 0000001f 00000025 00000029 0000002b 0000002f 00000035 0000003b 0000003d 00000043 00000047 00000049 0000004f 00000053 00000059 00000061 # último 0x61=97 0000001a # 26 primos encontrados ok # ocorreram mais de 10 interrupções core.vhd:875:7:@2275ns:(assertion failure): cMIPS BREAKPOINT at PC=000000c8 opc=010000 fun=100000 brk=10000000000000000000 SIMULATION ENDED (correctly?) AT exit(); /home/roberto/cMIPS/tb_cmips:error: assertion failed /home/roberto/cMIPS/tb_cmips:error: simulation failed
Execute a simulação com o GTKWAVE: prompt: run.sh -w -v cop0.sav Os sinais estão agrupados da seguinte forma, de cima para baixo: fetch: o sinal excp_pcsel escolhe o próximo PC durante uma excessão (PC, EPC, ErrorPC); nullify fica ativo=1 durante o início do processamento de uma excessão e de algumas instruções privilegiadas, tais como eret (ao final de interrupção); este sinal anula os efeitos das três instruções no 'início' do pipeline; interrupt_taken fica ativo=1 no ciclo em que a interrupção é aceita. Decode: exception_decode indica o código da instrução privilegiada que foi decodificada. Exec: mesmo que no laboratório anterior (entradas e saída da ULA). COP-0: sinais do coprocessador-0 definidos em vhdl/packageExcp.vhd; excode é o código da excessão (Tab 7-1 nas notas de aula); CAUSE, STATUS, EPC: valores dos respectivos registradores; int_req: pedido de interrupção, Memory: mesmo que no laboratório anterior (endereço, entrada e saída de dados do processador, wr=0, cpu_dVal=0). Write-back: wb_cop0_val é o valor "produzido" no COP-0 e que será armazenado no registrador de destino (wb_a_c). counter: counter_irq: pedido de interrupção=1; q é o valor da contagem. docs/cMIPS.pdf contém um diagrama de blocos com os sinais do COP-0. Para acompanhar a execução do programa você deve seguir o diagrama de tempos no GTKWAVE e ler a saída da compilação. Os endereços mostrados no código assembly são os endereços das instruções que são mostradas no topo diagrama de tempo, no conteúdo de PC. Vejamos como descobrir os endereços das funções e variáveis do seu programa. Um dos resultados da compilação é o arquivo extCounterInt.elf, que contém o "executável" do MIPS, que não pode ser interpretado diretamente pelo simulador. Este arquivo contém uma tabela com todos os nomes de símbolos (funções e variáveis) definidos nos três arquivos fonte (start.s, handlers.s e extCounterInt.c) usados para gerar o executável. O programa nm (names) mostra os nomes e valores dos símbolos: prompt: nm extCounterInt.elf mostra a tabela de símbolos definidos em extCounterInt.elf. Símbolos com 'T' pertencem à seção .text (funções), símbolos com 'D' pertencem à seção .data, com 'B' à seção .bss (dados não inicializados), e símbolos com 'a' são apelidos (aliases). Anote os endereços para HW_counter_addr _counter_saves _counter_val _excp_0200 excp_0200ret _start extCounter main readCounter startCounter stopCounter Vocês já sabem usar grep. Para acompanhar a simulação do código, use os endereços obtidos com nm mais a tradução do executável, obtida com a compilação verbosa (compile.sh -v). As instruções executadas antes de main() estão em include/start.s e inicializam o apontador de pilha e outros recursos do processador. No início de main() ocorrem 6 escritas na pilha, em endereços próximos ao topo da pilha, que está em (x_DATA_BASE_ADDR + x_DATA_MEM_SZ - 16). Estas constantes estão definidas em include/cMIPS.s. Em seguida, a variável _counter_val é inicializada. O contador é inicializado com a escrita no endereço HW_counter_addr. Note que a contagem q inicia em 0 e incrementa a cada ciclo. Desloque a simulação no tempo até que q se aproxime de 200 (0x0000.00c8). Assim que a contagem chega em 200 a interrupção é sinalizada, quando int_req passa de 0b0000.0000 para 0b0010.0000. A interrupção é aceita (interrupt_taken=1) e três instruções são anuladas (nullify=1). O EPC é carregado com o endereço da instrução que seria executada, não fosse a interrupção. CAUSE aponta a ocorrência da interrupção (b13=1, excode = b6..2=00000). STATUS passa para modo de excessão (exception level, b1=1), e a primeira instrução do tratador é buscada do endereço _excp_0200. O código fonte do tratador da interrupção do contador externo está em include/handlers.s. Inicia-se o tratamento da interrupção do contador, no endereço extCounter. O contador é reinicializado com a escrita de zero (endereço HW_counter_addr), o que causa a remoção do pedido de interrupção (int_req volta para 0b0000.0000). Na segunda escrita no contador, a contagem reinicia de 0. A variável _counter_val é lida e em seguida atualizada. Quando a instrução eret é executada, PC ← EPC, e o processador sai do exception mode. Acompanhe a sequência de eventos da segunda interrupção e verifique o que ocorre com os registradores STATUS, CAUSE e EPC. O tratador da interrupção do contador é extremamente simples, é executado com as interrupções desabilitadas, e usa somente os registradores k0 e k1. Se você encolher a escala de tempo (lupas com + e - no topo da janela do gtkwave), é possivel ver a ocorrência de todas as interrupções, observe as vezes em que counter_irq fica em 1. Segunda parte da tarefa.

Da busca de sinais no GTKWAVE

É possível procurar por valores nos diagramas de tempo do GTKWAVE:
  1. Selecione um sinal na lista "Signals" com um click do apontador; por exemplo, selecione o pc[31:0] ;
  2. no menu principal do GTKWAVE, em Search, selecione "Pattern Search 1";
  3. na janelinha que se abre, deixe "Logical Operation" em AND, mude o "Don't Care" para "String", e no quadro escreva o endereço da instrução procurada, que neste exemplo, é a primeira instrução de extCounter. Não altere as outras duas opções (Marking Begins/Stops);
  4. use os botões "Fwd" (forward) e "Bkwd" (backward) para percorrer o diagrama de tempos, buscando pela "string" para a frente no tempo, ou para trás.
  5. quando a "string" (o endereço de extCounter) é encontrado, o cursor fica no centro da tela do diagrama, indicando a ocorrência da "string".
  6. São dois os buscadores de padrão, e ambos podem ser usados, um para cada sinal; use o segundo buscador para procurar pelo endereço de main().

Documentação

O arquivo docs/cMIPS.pdf contém toda a documentação do cMIPS. Para além disso, veja o código fonte. --fim da aula--