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:
- 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:.
- include/handlers.s contém os tratadores de interrupções, um
para cada dispositivo.
- 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:
- Selecione um sinal na lista "Signals" com um click do apontador;
por exemplo, selecione o pc[31:0] ;
- no menu principal do GTKWAVE, em Search, selecione "Pattern Search 1";
- 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);
- 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.
- quando a "string" (o endereço de extCounter) é encontrado, o cursor
fica no centro da tela do diagrama, indicando a ocorrência da "string".
- 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--