-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathporte.tex
549 lines (387 loc) · 49.7 KB
/
porte.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
\chapter{Implementação dos Mediadores de Hardware}
\label{ch:porte}
Nesta seção será discutido como foi o porte de cada mediador de hardware, explicando as decisões tomadas.
O processo de inicialização do sistema envolve as seguintes ações \cite[p.~110]{ug585.1.7}:
\begin{enumerate}
\item Definir a tabela de vetores.
\item Invalidar \emph{caches}, TLB, \emph{branch predictor}
\item Preparar tabelas de tradução de página.
\item Configurar as pilhas dos diferentes modos de execução.
\item Carregar o endereço base da tabela de páginas no registrador apropriado para ser usado pela MMU.
\item Ativar a MMU.
\item Ativar a L2, ativar a L1.
\item Pular para o ponto de entrada a aplicação.
\end{enumerate}
Estes e outros passos (como inicialização do GIC, timer, UART, CPU, etc) são explicados em detalhe nas seções que seguem. O primeiro passo a ser feito para iniciar um novo porte é incluir em \verb+./include/system/types.h+ os nomes das classes dos mediadores que serão implementados e criar as pastas necessárias para colocar estas classes, no caso deste porte, foram criadas as pastas \verb+./include/mach/zynq/+, \verb+./src/mach/zynq/+, \verb+./include/arch/armv7/+ e \verb+./src/arch/armv7/+.
\section{Mediador da UART}
A inicialização da UART foi feita de acordo com o sugerido pelo manual em \cite[p.~554]{ug585}. Nesta seção será comentado as decisões tomadas na configuração inicial da UART, em particular por causa dos momentos em que o manual exigia que o desenvolvedor tomasse uma decisão.
A primeira decisão que foi necessária é a de escabelecer qual será a taxa de transmissão (\emph{Baud Rate}) da UART.
\begin{figure}[ht!]
\centering
\includegraphics[width=10cm]{figuras/uart_board_rate}
\caption{Esquemático de como é criada a taxa de transmissão.}
\label{fig:uart}
\end{figure}
Primeiramente foi necessário configurar o \emph{clock} de referência da UART. Para isto, deve-se dividir o \emph{clock} que vem do I/O PLL (que por sua vez deriva do PS\_CLK, que é o \emph{clock} geral do sistema). Recomenda-se dividir o \emph{clock} da I/O PLL de modo a se obter 50 ou 33 MHz.
O \emph{clock} da I/O PLL, por padrão, multiplica o PS\_CLK (de 33.33 MHz) por 26, resultando num \emph{clock} de 866 MHz.
No manual diversas vezes é usado como exemplo para o \emph{clock} de referência (que é mostrado sob o nome de \verb+UART Ref clock+ na figura \ref{fig:uart}) da UART como 50 MHz, portanto, arbitrariamente, escolheu-se esse valor. Logo, deve-se configurar o registrador UART\_CLK\_CTRL, responsável por configurar o \emph{clock} de entrada da UART, para dividir este \emph{clock} vindo da I/O PLL por 17 ($866/17 = 50$).
Agora, com este \emph{clock} estabelecido, que vamos chamar de sel\_clk (de acordo com a nomenclatura do manual), devemos calcular quanto será a taxa de transmissão.
Após alguma pesquisa em fóruns de desenvolvedores de software básico, foi observado que uma taxa de transmissão de 9600 bps é bastante comum, portanto, assumindo este valor, a próxima etapa é configurar os dois divisores de \emph{clock} que ajustam a taxa de transmissão, como indicado na figura \ref{fig:uart}.
O primeiro divisor chama-se CD (\emph{clock divider}), que configura a constante para se dividir o \emph{clock}, e BDIV um segundo divisor usado para sobreamostragem, organizado como mostrado na figura \ref{fig:uart}. O valor da taxa de transmissão final é calculado da seguinte forma:
\begin{equation}
\text{taxa de transmissão} = \frac{sel\_clk}{CD \times (BDIV+1)}
\end{equation}
O valor padrão de BDIV é 15, portanto, fixando-se este valor e resolvendo a equação por CD, temos que $CD = 325$. Configurando-se estes valores nos seus respectivos registradores, obtém-se a taxa de transmissão desejada de 9600 bps.
Após estas configurações, dentre outras que o manual descreve, a UART está pronta para ser usada. Os dois principais métodos usados da UART são o \verb+put+ e o \verb+get+, o primeiro escreve um caractere na saída serial, sendo que este pode ser lido, por exemplo através de uma entrada USB, para imprimir estes caracteres numa tela; algo muito útil para depuração.
\section{Mediador do \emph{Timer}}
Um \emph{timer} permite contar um certo número de ciclos, e, ao final da contagem, ele emite uma interrupção ao processador, para que então o processador trate este evento. Entretanto note que é possível de existir mais \emph{timers} sendo usados logicamente do que \emph{timers} físicos disponíveis, significando que um mesmo \emph{timer} deve conseguir servir a mais de uma requisição simultaneamente.
Portanto não podemos apenas configurar um \emph{timer} para contar initerruptamente até passar o tempo que desejamos, do contrário novas requisições sobreescreveriam a anterior. Para ilustrar, suponha que se queira contar por 10 segundos, como o \emph{clock} do \emph{timer} é de 333 MHz (periodo $\frac{1}{333 \times 10^6}$s), bastaria configurar o \emph{timer} para contar por $10 \times 333 \times 10^6$ ciclos e então chamar o \emph{handler} associado à interrupção gerada quando o \emph{timer} chegar em zero.
Agora imagine que, no cenário acima, enquanto o \emph{timer} ainda está servido àquela solicitação de contagem, apareça outra solicitação, de um alarme por exemplo, e queria contar por 20 segundos. Se esta solicitação sobrescrever o registrador de configuração do \emph{timer}, a solicitação anterior não terá seu pedido atendido a tempo. Note também que o escalonador de processos também estará usando este \emph{timer}.
Para resolver este problema, na arquitetura do EPOS existe o conceito de ticks (algo parecido com o que se faz no Linux), onde se configura um \emph{timer} para gerar interrupções em um intervalo regular, intervalo este que deve ser pequeno o suficiente para poder atender a demanda de contagens de pequenos valores, assim como não ser pequeno demais ao ponto de gastar mais processamento tratando as interrupções geradas pelo \emph{timer} do que servindo à outras funções. Assim, cada objeto que instancia (ou usa) um \emph{timer}, como o Alarm, Scheduler e Chronometer, nunca realmente tocam em algum registrador do \emph{timer} (portanto esses componentes são independentes de arquitetura), e, no lugar disso, computam quantos ticks, isto é, quantas interrupções de \emph{timer} aconteceram.
Para exemplificar o funcionamento destes componentes, tomemos o escalonador de processos. No construtor do escalonador, é enviado como parâmetro o periodo de escalonamento, ou seja, quanto tempo (no máximo) uma \emph{thread} pode executar antes de ser escalonada. Para se saber quantos ticks devem ser contados antes de se escalonar um processo, basta dividir a frequência em que os ticks incrementam, pela frequência de escalonamento. Por exemplo, se o \emph{timer} gera uma interrupção a cada 1 milisegundo (1000 Hz), e o escalonador escalona um processo a cada 10 milisegundos (100 Hz), o número de ticks a se contar é $1000/100 = 10$ ticks. Estes são os valores usados na implementação também.
No caso do Alarm em específico, internamente há uma fila com todas as requisições de alarme, ordenado do menor tick ao maior. Quando ocorre uma interrupção de \emph{timer}, é chamado primeiramente o \emph{handler} que gerencia esta fila, e, caso um alarme desta fila já tenha esperado os ticks que requisitou, então o \emph{handler} desse alarme é chamado (este \emph{handler} é definido pelo usuário que instanciou o alarme).
O construtor do \verb+Zynq_Timer+ recebe como parâmetro a frequência que o contador deve contar, assim como o \emph{handler} que deve ser chamado quando esta contagem terminar (isto é, a função chamada quando acontecer uma interrupção devido ao \emph{timer} ter chego a zero), e um número chamado \verb+channel+, que serve para demultiplexar qual \emph{handler} deve ser chamado.
A classe \verb+Zynq_Timer+ possui um atributo estático (e portanto único para todas as instâncias) definido como \verb+Zynq_Timer*+ \verb+_channels[CHANNELS]+, onde \verb+CHANNELS+ é uma constante com o número de diferentes classes usando o \emph{timer} (Scheduler e Alarm). Este vetor é necessário pois, quando uma interrupção de \emph{timer} acontece e o \emph{handler} do \emph{timer} é chamado, este \emph{handler} pode iterar sobre ele, chamando todos os respectivos \emph{handlers} daquelas classes. Abaixo segue um exemplo de código de como isto pode ser feito:
\label{int_handler}
\begin{lstlisting}
Zynq_Timer * Zynq_Timer::_channels[CHANNELS];
void Zynq_Timer::int_handler(IC::Interrupt_Id id)
{
if((!Traits<System>::multicore || Machine::cpu_id() == 0)
&& _channels[ALARM])
{
_channels[ALARM]->_handler();
}
if(_channels[SCHEDULER] &&
(--_channels[SCHEDULER]->_current[Machine::cpu_id()] <= 0))
{
_channels[SCHEDULER]->_current[Machine::cpu_id()] =
_channels[SCHEDULER]->_initial;
_channels[SCHEDULER]->_handler();
}
}
\end{lstlisting}
No código, \verb+_current+ guarda o número de ticks atual daquele \emph{timer} (para cada processador), e \verb+_initial+ guarda o número inicial de ticks que foi configurado na inicialização do do escalonador. Note então que a cada interrupção de \emph{timer}, o tratador do alarme será chamado (no caso do primeiro processador), já o do escalonador, somente a cada certo número de ticks ele será chamado (cerca de 10 na implementação).
Os 4 principais registradores a se trabalhar para configurar o \emph{timer} são o \verb+load+, registrador onde se escreve por quantos ciclos se deve contar; o \verb+counter+, que é o registrador que contém o atual valor contado, sendo decrementado a cada ciclo até chegar em zero, chegando em zero o é gerada uma interrupção número 29; registrador \verb+control+, que permite configurar certos comportamentos do \emph{timer}, como o de ativa-lo, ativar modo cíclico, ativar interrupções e atribuir um valor para o \verb+prescale+; e finalmente o registrador \verb+interrupt status+, que, como o nome indica, permite que se leia o status das interrupções de timer. Todos os timers trabalham à metade do \emph{clock} do sistema, ou seja, usando o \emph{clock} CPU\_3x2x.
Durante a inicialização do sistema, o \emph{timer} é configurado para gerar interrupções periodicamente, e esta configuração não é alterada durante a execução da aplicação. Como o construtor do \verb+Zynq_Timer+ recebe uma frequência como parâmetro, é necessário converter esta frequência para um número de ciclos a se contar. Para isto, é necessário levar em conta a frequência com que o contador é decrementado, para então se definir um valor a ser decrementado periodicamente de modo a fornecer a frequência desejada.
Como sabemos que o \emph{clock} ao qual o \emph{timer} está submetido é metade do \emph{clock} do sistema, e que antes dele chegar ao contador, este mesmo \emph{clock} é dividido por um divisor chamado \verb+prescaler+ (que divide pelo valor configurado nele mais 1), podemos dizer a frequência F\_dec de decremento do contador é de:
\begin{equation}
\text{F\_dec} = \frac{\text{CPU\_6x4x}}{2 \times (PRESCALER+1)}
\end{equation}
Logo, usando a mesma linha de racioncínio exposta no exemplo de cálculo de ticks, temos que o valor a ser carregado no \verb+load_register+ (que será chamado de \verb+load_value+), sendo F a frequência que se deseja configurar o \emph{timer}, é:
\begin{equation}
\text{load\_value} = \frac{\text{CPU\_6x4x}}{2 \times (PRESCALER+1) \times F}
\end{equation}
Ou de maneira mais simples:
\begin{equation}
\text{load\_value} = \frac{\text{F\_dec}}{F}
\end{equation}
Precisamos agora definir o \verb+prescaler+. Definimos ele como a razão entre o \emph{clock} do sistema pela frequência desejada ($\text{prescaler}=\frac{\text{clock}}{2 \times F}$). Há a premissa de que a frequência desejada não será maior que a do \emph{clock} do \emph{timer}, pois é impossível contar mais rápido que isto. Normalmente esta razão ($\frac{\text{clock}}{2 \times F}$) será um número maior que 255, já que o clock costuma ser muito mais rápido, e como o campo onde se registra o valor do prescaler possui apenas 8 bits, frequentemente o prescaler será 255.
\section{Mapeamento de Memória}
\label{sec:map}
Por padrão, as 8 primeiras palavras da memória (ou seja, os primeiros $8 \times 4 = 32$ bytes) devem possuir instruções específicas. A primeira palavra (memória posição 0) contém a primeira instrução a ser executada, e, nas 7 palavras seguintes, fica a tabela de vetores (\emph{vector table}). Como abaixo da instrução inicial há uma tabela que não se deseja executar no momento de inicialização do sistema, esta primeira instrução necessariamente é um \emph{jump} para uma outra região, para aí então se iniciar o processo de \emph{boot}.
As demais 7 palavras, pertencentes à tabela de vetores possuem, similarmente, \emph{jumps} para o código onde o tratador da exceção se localiza. A primeira instrução da tabela (posição 0x4) deve conter um \emph{jump} o tratador de uma exceção do tipo undefined instruction, depois, nas próximas palavras, a software interruption, prefectch abort, data abort, reserved, irq e, finalmente, fiq, nesta ordem. Vide seções \ref{sec:interrupt} e \ref{sec:operating_modes} para mais detalhes.
Desmontando-se o binário da imagem produzida na compilação do EPOS (\emph{dump}), deve-se obter uma saída semelhante a esta exemplificada abaixo em seus primeiros 32 bytes:
\label{dump}
\hspace*{-1cm}\vbox{
\begin{verbatim}
00000000 <_vector_table>:
0: ldr pc, [pc, #2044] ; 804 <_start_addr>
4: ldr pc, [pc, #2044] ; 808 <_undefined_instruction_addr>
8: ldr pc, [pc, #2044] ; 80c <_software_interrupt_addr>
c: ldr pc, [pc, #2044] ; 810 <_prefetch_abort_addr>
10: ldr pc, [pc, #2044] ; 814 <_data_abort_addr>
14: ldr pc, [pc, #2044] ; 818 <_reserved_addr>
18: ldr pc, [pc, #2044] ; 81c <_irq_handler_addr>
1c: ldr pc, [pc, #2044] ; 820 <_fiq_handler_addr>
\end{verbatim}
}
Também foi necessário definir as pilhas (\emph{stacks}) do sistema, assim como reservar um espaço para a pilha dos tratadores de interrupção. O \emph{layout} escolhido segue na tabela \ref{tab:stacks}.
Este é o \emph{layout} usado durante o desenvolvimento. Somente as pilhas de usuário e de algum modo privilegiado (como o supervisor) são necessárias, portanto a pilha de supervisor acabaria por englobar as demais posições que aparecem na tabela \ref{tab:stacks}. Na próxima seção é explicado como foi feito para não precisar reservar espaço na memória para as demais pilhas.
\begin{table}[hb]
\centering
\begin{tabular}{ccc}
\hline \hline
Pilha & Endereço base & Tamanho máximo (bytes)\\[0.5ex]
\hline
Supervisor (CPU1) & \verb+0x00080000+ & 500784\\
Supervisor (CPU0) & \verb+0x00100000+ & 524288\\
Irq & \verb+0x00100040+ & 64\\
System & \verb+0x00100080+ & 64\\
Abort & \verb+0x001000c0+ & 64\\
Fiq & \verb+0x00100100+ & 64\\
Undefined & \verb+0x00100140+ & 64\\[1ex]
\hline
\end{tabular}
\caption{Pilhas do sistema com seus tamanhos e posições.}
\label{tab:stacks}
\end{table}
Lembrando que pilhas, num sistema operacional, tradicionalmente crescem em direção à posições menores da memória, por isto que, por exemplo, a pilha Irq possui 64 bytes, já que $\texttt{0x100040}-\texttt{0x100000} = 40_{16} = 64_{10}$. A pilha do usuário, portanto, localiza-se na última posição da memória (512 MB neste caso) e cresce para ``baixo'' (posições menores de memória) a partir de lá.
A tabela \ref{tab:mem} ilustra o restante do mapeamento de memória feito. APP\_DATA é a posição onde será armazenado a seção de dados do EPOS, sendo que 1KB é seguramente mais do que o suficiente para esta seção. É possível descobrir qual é o tamanho da seção de dados em um binário através do comando \verb+size <binary>+. Com este comando, observou-se que, na atual versão do EPOS, a seção de dados possui 436 bytes, e a .bss 188 bytes.
System Info é uma \verb+struct+ que guarda informações sobre o SO, que é usado durante a inicialização do sistema (e pode ser descartado mais tarde).
Para a MMU, é necessário 3600 KBs para mapear todo o espaço de endereçamento virtual (vide a seção \ref{init_pages} para entender porque apenas 3600 KBs); a tabela e páginas usada pela MMU está indicada como ``Tabela MMU'' na tabela \ref{tab:mem}.
``Livre'' indica memória não usada. Esta porção não pode ser alocada pois a tabela da MMU precisa iniciar numa posição de memória alinhada à 16 KB.
SYS\_HEAP é a \emph{heap} do sistema operacional. Note que as pilhas das \emph{threads} ficam dentro da \emph{heap} do sistema, por isto ele deve ter um tamanho adequado para poder alocar memória suficiente para o número de \emph{threads} que espera-se executar.
% APP\_STACK é a pilha da aplicação; note que esta pilha cresce em direção contrária a da \emph{heap}, portanto um excesso de uso da \emph{heap} ou da pilha pode acabar corrompendo a memória da aplicação, gerando um erro que são chamados em alguns sistemas operacionais de \emph{stack overflow}.
\begin{table}[ht]
\centering
\begin{tabular}{ccc}
\hline \hline
Dado & Endereço base & Tamanho Alocado\\[0.5ex]
\hline
APP\_DATA & \verb+0x00100140+ & 1KB\\
System Info & \verb+0x00100540+ & 260 bytes\\
Livre & \verb+0x00100644+ & ~14KB\\
Tabela MMU & \verb+0x00104000+ & 3600KB\\
SYS\_HEAP & \verb+0x00488000+ & 128MB\\[1ex]
% APP\_STACK & \verb+0x1ffffffc+ & \~380MB
\hline
\end{tabular}
\caption{Mapeamento de memória.}
\label{tab:mem}
\end{table}
Estes valores são editáveis nos \verb+traits+, entretanto também foi necessário atualizar o arquivo \verb+./include/mach/zynq/memory_map.h+.
\section{Controlador de Interrupções}
Para se usar o controlador de interrupções (que será referenciado como GIC no restante desta seção, de \emph{Generic Interrupt Controller}), é necessário antes inicializar o distribuidor de interrupções e as interfaces dos processadores.
É no distribuidor que é determinada a prioridade de cada interrupção, e onde é decidido se determinada interrupção deve ou não ser encaminhada para a interface de um determinado processador. Todas as interrupções passam por ele.
A interface dos processadores é por onde os processadores se comunicam com o GIC. Nela o processador pode confirmar que recebeu a interrupção (\emph{acknowledge}), indicar que terminou de tratar a mesma, definir prioridades entre diferentes interrupções, indicar uma política de preempção de interrupções, ou mesmo desligar esta interface. Como o GIC é dividido logicamente é ilustrado na imagem \ref{img:gic}.
\begin{figure}[ht!]
\centering
\includegraphics[width=9.5cm]{figuras/gic}
\caption{Divisão lógica do GIC.}
\label{img:gic}
\end{figure}
\subsection{Inicialização}
Na inicialização do distribuidor, através dos registradores mapeados em memória de configuração do mesmo, é definido, para cada uma das possíveis interrupções, se elas são \emph{level-sensitive} ou \emph{edge-triggered}.
Em seguida configura-se a prioridade de cada interrupção. A princípio todas as interrupções foram definidas como tendo a mesma prioridade, mas isto é configurável caso necessário.
É configurado então o processador-alvo de cada interrupção, isto é, para quais interfaces de processador uma determinada interrupção será encaminhada. Finalmente então são ativadas as interrupções \cite{gic}.
A inicialização da interface do processador é mais simples. Primeiro se configura a máscara de prioridade da CPU, isto é, qual é o nível de prioridade mínimo que uma interrupção precisa ter para interromper aquele processador. Na implementação, esta máscara está desativada. Depois configura-se a política de grupos de preempção. No GIC é possível separar interrupções em grupos de preempção, onde é definido se determinado grupo pode preemptar determinado outro grupo. Todas as interrupções foram colocadas no mesmo grupo e interrupções podem ser preemptadas.
Adicionalmente a estas configurações, é possível de mascarar as interrupções FIQ e IRQ através do CPSR (\emph{Current Program Status Register}), alterando-se os bits 6 e 7 dele, para mascarar interrupções FIQ e IRQ, respectivamente. Normalmente é o que é feito quando é necessário mascarar as interrupções temporariamente, enquanto se mantém as configurações do GIC.
\subsection{Fluxo de execução ao se receber uma interrupção}
O processador, ao receber uma interrupção, (portanto as interrupções estão ativas e não mascaradas pela interface ou pelo CPSR), para a execução do código que estava executando e então executa a instrução contida na tabela de vetores (mostrada na página \pageref{dump}) correspondente ao tipo de interrupção recebida. Esta instrução é um \emph{jump} para um tratador (\emph{handler}) daquele tipo de interrupção.
O principal tratador é o tratador de interrupções IRQ (\verb+irc_handler+), sendo que este precisa ser discutido com mais profundidade. Abaixo segue o código deste tratador:
\hspace*{-1cm}\vbox{
\begin{lstlisting}
43 void _irq_handler() {
44 ASMV(
45 // A few definitions
46 ".equ ARM_MODE_FIQ, 0x11 \n"
47 ".equ ARM_MODE_IRQ, 0x12 \n"
48 ".equ ARM_MODE_SVC, 0x13 \n"
49 ".equ IRQ_BIT, 0x80 \n"
50 ".equ FIQ_BIT, 0x40 \n"
51 // go to SVC
52 "msr cpsr_c, #ARM_MODE_SVC | IRQ_BIT | FIQ_BIT \n"
53 //save current context (lr, sp and spsr are banked registers)
54 "stmfd sp!, {r0-r3,r12,lr,pc}\n"
55
56 "msr cpsr_c, #ARM_MODE_IRQ | IRQ_BIT | FIQ_BIT\n"//go to IRQ
57
58 "sub r0, lr, #4 \n" // return from irq addr
59 "mrs r1, spsr \n" // pass irq_spsr to svc r1
60 //go back to SVC
61 "msr cpsr_c, #ARM_MODE_SVC | IRQ_BIT | FIQ_BIT\n"
62 "add r2, sp, #24 \n" //sp+24 is the position of the saved pc
63
64 // save return address into the pc position
65 "str r0, [r2] \n"
66 "stmfd sp!, {r1} \n" // save irq-spsr
67
68 );
\end{lstlisting}
\begin{lstlisting}
71 IC::int_handler();
72
73 ASMV(
74 "ldmfd sp!, {r0} \n"
75 "msr spsr_cfxs, r0\n"//restore IRQ's spsr value
76 //to SVC's spsr
77 "ldmfd sp!, {r0-r3,r12,lr,pc}^ \n" // restore context
78 //the ^ in the end of the above instruction makes the
79 //spsr to be restored into svc_cpsr
80 );
81 }
\end{lstlisting}
}
Após selecionar qual handler chamar do vetor de exceções, o processador muda de modo, indo, no caso de uma interrupção IRQ, para o modo de execução IRQ. Neste modo há 3 registradores banqueados: O SPSR, que contém o valor do registrador CPSR imediatamente antes da interrupção, sendo necessário para que seja possível restaurar o valor original do CPSR após tratar a interupção; o LR (\emph{link register}), que contém o endereço da próxima instrução que seria executada imediatamente antes da interrupção mais 4; e, finalmente, o SP (\emph{stack pointer}), que aponta para a pilha própria desde modo (cada modo pode possuir sua própria pilha).
Para evitar desperdício de memória reservando uma pilha própria apenas para este modo, optou-se por não usar uma pilha no modo IRQ, e, no lugar disto, usar sempre a mesma pilha do modo supervisor (que é o modo de execução do processador quando ele inicia), isto também permite um melhor gerenciamento da pilha no caso de preemptação. Para isto, a primeira instrução a executar é uma mudança de modo para voltar ao modo supervisor, enquanto mantendo novas interrupções desligadas; lá é salvo o contexto na pilha daquele modo. Entretanto para que seja possível restaurar o fluxo de execução no mesmo estado em que o processador estava no momento imediatamente antes da interrupção, é necessário voltar ao modo IRQ para obter-se os valores contidos dos registradores banqueados SPSR e LR; após isto, pode-se então voltar ao modo \emph{supervisor}. Na linha 62 do código é somado 24 à pilha pois lá é a posição de memória onde está salvo o PC após ele ter sido empilhado na linha 54, e deseja-se sobreescrever aquele valor do PC pelo valor que estava contido no LR do modo IRQ (menos 4), pois aquela é a próxima instrução que seria executada antes da interrupção. Feito isto, salva-se o valor do SPSR do modo IRQ no topo da pilha (que é o valor do CPSR pré-interrupção), para ser restaurado ao CPSR mais tarde.
Agora que o contexto foi salvo corretamente para ser restaurado, pode-se então chamar um tratador de interrupções genérico (que será discutido mais a frente) e escrito em C++. Após o \verb+int_handler+ da linha 71 retornar, é feita a restauração do contexto. Primeiro se salva o CPSR salvo na pilha no SPSR, na última instrução (\verb+ldmfd+), na forma em que ela está (com um \^{} no final dela e com o PC na lista), ela automaticamente restaurará o valor que está no SPSR para o CPSR. Como o PC está na lista, o fluxo de execução terá retornado a executar as instruções de antes da interrupção ocorrer.
Agora será discutido como que as interrupções são tratadas individualmente. O corpo do \verb+int_handler+ é bastante curto, então vale a pena escreve-lo aqui:
\begin{lstlisting}
void Zynq_IC::int_handler()
{
unsigned int icciar_value = CPU::in32(IC::GIC_PROC_INTERFACE
| IC::ICCIAR);
IC::Interrupt_Id id = icciar_value & IC::INTERRUPT_MASK;
if(id == 1023){
kout << "Spurious interruption received\n";
return;
}
_vector[id](id);
CPU::out32(IC::GIC_PROC_INTERFACE | IC::ICCEOI, icciar_value);
}
\end{lstlisting}
A primeira coisa que o tratador faz é descobrir qual é o número da interrupção que foi gerada, para assim saber como tratar aquela interrupção. Isto é feito lendo-se o registrador ICCIAR (\emph{Interrupt Acknowledge Register}), que provê o número da interrupção e também o processador endereçado.
É possível que uma interrupção já tenha sido tratada por outro processador, quando isto acontece, o GIC emite uma \emph{Spurious Interruption} para indicar isto. Quando se detecta isto, o tratador não precisa tomar nenhuma outra ação, basta retornar a execução normal.
Com o número da interrupção em mãos, pode-se então chamar o tratador daquele tipo de interrupção. O \verb+_vector+ é um vetor de \emph{handlers}, onde para cada posição $i$, existe o tratador da interrupção número $i$. Para sinalizar que uma interrupção foi tratada, deve-se escrever no registrador ICCEOI (\emph{End of Interruption}) o número lido no ICCIAR (ou seja, o número da interrupção e processador de destino).
Normalmente, interrupções de \emph{timer} são o tipo mais frequente de interrupção durante a execução do sistema. Dele dependem o escalonador de processos (ou \emph{threads}), Delay, Chronometer e Alarm. Como mencionado na seção do porte do \emph{timer}, uma mesma interrupção pode gerar a chamada de mais de um handler, como a do \emph{timer} que chama a do Alarm e do escalonador. Com isto fica ilustrado que uma única interrupção pode gerar a chamada de vários tratadores para esta mesma interrupção.
\section{MMU}
\label{sec:mmu}
%http://www.slideshare.net/prabindh/enabling-two-level-translation-tables-in-armv7-mmu
%http://www.embedded-bits.co.uk/2011/mmucode/
%http://lxr.free-electrons.com/source/arch/arm/mm/proc-v7-2level.S?a=arm
As principais funções de uma MMU (\emph{Memory Managemend Unit}) são proteção de memória, ou seja, não permitir acesso a certas regiões da memória por processos não autorizados, e de fazer o mapeamento de memória virtual para memória física, mapeamento este que facilita a escrita de aplicações já que o desenvolvedor dela não precisará estar ciente de como a memória é mapeada internamente pelo sistema operacional.
A MMU consegue cumprir estes dois objetivos (e outros mais) através do uso de uma Tabela de Tradução de Páginas (que chamaremos de TTP), onde, a grosso modo, cada linha desta tabela é indexada pelo valor do endereço virtual, e na entrada correspondente há o endereço físico assim como \emph{flags} que indicam, dentre outras coisas, se aquela região pode ou não ser acessada pela aplicação em execução.
Esta tabela é salva na memória principal (RAM) do sistema, e configurar a MMU significa especialmente criar métodos para gerenciar e popular esta tabela. Para entender como isto é feito, será explicado agora como é estruturada esta tabela, como que ocorre a tradução de memória virtual para física, e o que cada entrada da tabela deve ter.
\subsection{Estrutura da Tabela de Tradução de Página}
A TTP possui dois níveis, portanto, desconsiderando-se a TLB, é necessário dois acessos à TTP para traduzir um endereço, caso configurado para páginas de 4 KB. a MMU suporta páginas de 4 KB e 64 KB, assim como seções de 1 MB e 16 MB. Como cada endereço virtual corresponde à exatamente uma entrada na TTP, usar páginas grandes reduz o tamanho da TTP (consequentemente o uso de memória por ela), entretanto usar páginas menores (4 KB) melhoram muito a eficiência da alocação dinâmica de memória e desfragmentação \cite[p.~77]{ug585.1.7}, porém mapear um espaço de 4 GB exigiria milhões de entradas na tabela. Foi justamente para conciliar estes fatores, que decidiu-se por uma tabela de dois níveis, como será melhor ilustrado a seguir.
A tabela de tradução páginas nível um (TTP1), também chamada de ``\emph{master table}'', divide todo o espaço de endereçamento de 4 GB em 4096 seções de 1 MB, portanto, como cada entrada desta tabela tem o tamanho de uma palavra, seu tamanho total em memória é de $4 \times 4096 = 16$ KB. Os 2 bits menos significativos (lsb) de cada entrada desta tabela, definem que tipo de entrada ela é, que pode ser um dos 5 tipos:
\begin{itemize}
\item Bits lsb 00: É uma ``fault entry'', e gera uma exceção do tipo \emph{prefetch} ou \emph{data abort}, dependendo do tipo de acesso. Este tipo de entrada indica que não há mapeamento virtual para esta região.
\item Bits lsb 01: Indica a posição de memória de uma tabela de páginas (nível 2).
\item Bits lsb 10 com bit 18 em 0: Aponta para uma seção de 1 MB.
\item Bits lsb 10 com bit 18 em 1: Indica uma seção de 16 MB, este tipo de entrada exige 16 posições na TTP1.
\item Bits lsb 11: Reservado, gera faltas de tradução e não deve ser usado.
\end{itemize}
A figura \ref{fig:l1_entry} ilustra o formato de uma entrada da TTP1. NS significa \emph{non-secure}, e só tem semântica com extensões de segurança habilitadas (fora do escopo do trabalho). SBZ significa \emph{should be zero}. Bits C, B, TEX e AP serão explicados após a explicação do formato de uma entrada da TTP2, pois esta tem campos análogos.
\begin{figure}[hb!]
\centering
\includegraphics[width=9cm]{figuras/l1_entry}
\caption{Formato de uma entrada da tabela de tradução de páginas nível 1.}
\label{fig:l1_entry}
\end{figure}
Vamos agora entender como se traduz de um endereço virtual, o endereço de uma TTP2. No coprocessador CP15, existe um registrador chamado TTBR (dois na realidade, TTBR0 e TTBR1), sigla de \emph{Translate Table Base Register}, onde é guardado o endereço da primeira posição da TTP1. Com a MMU ligada, quando há um acesso à memória, automaticamente a MMU pulará para a posição indicada no TTBR, e indexará esta tabela usando os 12 bits mais significativos do endereço virtual. A maneira exata de como é feito este cálculo é mostrada na figura \ref{fig:translation}.
Na posição de memória encontrada no passo anterior, estará uma entrada da TTP1 no formato indicado na figura \ref{fig:l1_entry}. No caso de uma entrada que indica uma outra TTP, os primeiros 22 bits indicam o endereço onde estará esta tabela, com os demais bits em 0, com exceção do último e os do campo de domínio (que será explicado a seguir). Com estas informações, a MMU já pode localizar a posição da TTP2. Para saber qual das entradas da TTP2 deve ser selecionada, é usado os bits [19:12] do endereço virtual para indexar a TTP2.
Os 4 bits do campo de domínio indicam em qual dos possíveis 16 domínios de memória aquela região se encontra, um domínio é apenas um conjunto de regiões de memória. A utilidade disto é que se pode, para cada domínio, especificar como será o controle de acesso nele. Cada domínio pode estar configurado (pelo registrador DACR) como um dos três tipos de acesso: \emph{Acesso não permitido}, qualquer acesso a esta região gera uma falta de domínio; \emph{Cliente}, permissões de acesso são verificadas, e podem gerar uma falta de permissão; e \emph{Administradores}, onde permissões de acesso não são verificadas (bits AP e APx são ignorados).
\begin{figure}[h]
\centering
\includegraphics[width=9cm]{figuras/translation}
\caption{Processo de tradução de memória virtual para física.}
\label{fig:translation}
\end{figure}
A TTP2 possui 256 entradas, cada uma com o tamanho de uma palavra, logo, cada TTP2 precisa de 1 KB de memória. Note que como uma entrada da TTP1 provê 22 bits para endereçar uma TTP2, esta TTP2 pode efetivamente estar localizada em qualquer região da memória, já que 22 bits são suficientes para indexar qualquer região de 1 KB de memória.
A figura \ref{fig:l2_entry} ilustra como é o formato de uma entrada na TTP2. Os 2 bits menos significativos indicam se a página possui 4 KB, 16 KB ou se a região não está mapeada. Com o endereço de uma entrada da TTP2 em mãos, basta combinar os 20 bits mais significativos desta entrada com os 12 bits menos significativos do endereço virtual, e teremos traduzido o endereço virtual para um físico.
\begin{figure}[h]
\centering
\includegraphics[width=9cm]{figuras/l2_entry}
\caption{Processo de tradução de memória virtual para física.}
\label{fig:l2_entry}
\end{figure}
Agora vamos discutir o que cada campo de uma entrada da TTP2 significa.
\textbf{AP e APx:} São os bits que codificam o tipo de acesso que aquela porção de memória possui. Estes bits só são checados caso do domínio seja de \emph{Cliente}. Um acesso que não possui as permissões necessárias geram uma exceção do tipo \emph{prefetch} ou \emph{data abort}. APx e AP[2] são sinônimos. A tabela \ref{tab:apx} ilustra os diferentes tipos de acesso que podem ser codificados.
\begin{table}[h]
\centering
\begin{tabular}{ccccc}
\hline \hline
APx & AP[1] & AP[0] & Privilegiado & Não Privilegiado\\[0.5ex]
\hline
0 & 0 & 0 & Sem acesso & Sem acesso\\
0 & 0 & 1 & Escrita/Leitura & Sem acesso\\
0 & 1 & 0 & Escrita/Leitura & Sem acesso\\
0 & 1 & 1 & Escrita/Leitura & Sem acesso\\
1 & 0 & 0 & \~{} & \~{} \\
1 & 0 & 1 & Leitura & Sem acesso \\
1 & 1 & 0 & Leitura & Leitura \\
1 & 1 & 1 & \~{} & \~{} \\[1ex]
\hline
\end{tabular}
\caption{Codificações das permissões de acesso.}
\label{tab:apx}
\end{table}
\textbf{TEX:} Controla políticas de compartilhamento de memória, caso aquela região seja compartilhada.
\textbf{C e B:} Estes campos combinados controlam a política de ``cacheamento'' daquela região de memória, ou seja, se ela pode ser salva na cache, e se pode, qual o algoritmo a a ser usado, como \emph{write-back} ou \emph{write-though}, e também políticas de \emph{write-allocate}.
\textbf{S:} Bit que determina que aquela porção de memória é compartilhada entre processadores.
\textbf{nG:} Responsável pela proteção de memória intraprocessos. Uma região marcada como não global (nG = 1) tem associado à ela um número que o SO atribui a cada processo. Isto permite que a TLP possa ter diferentes mapeamentos simultaneamente sem necessidade de substituir uma entrada.
\textbf{xN:} Bit que marca a região como não executável. É importante por questões de segurança, já que o \emph{fetch} especulativo de instruções pode ler uma porção de memória que possui dados sensíveis.
\subsection{Inicializando a Tabela de Páginas}
\label{init_pages}
Agora que sabemos como as TTPs são estruturadas, bem como o formato de cada entrada dela, podemos finalmente popular esta tabela. Há várias estratégias para fazer isto \cite{mmutheory}, dependendo da forma desejada de mapeamento, por exemplo:
\begin{itemize}
\item Mapeamento por Demanda: Pode-se fazer com que determinado endereço virtual não possua um mapeamento correspondente a priori, gerando uma exceção que é tratada criando-se uma TTP2 que mapeie aquela área.
\item Mapeamento 1 para N: Com o advento do bit nG, é possível também mapear uma mesma área de memória virtual em diferentes áreas da memória física, de modo que quando o escalonador de processos escalona num novo processo (ou \emph{thread}), ele também modifica o mapeamento de memória, deste modo, diferentes processos poderiam usar uma mesma determinada posição de memória virtual.
\item Mapeamento 1 para 1: É o mapeamento mais simples possível, de modo que a posição de memória virtual X corresponde a posição de memória física X.
\end{itemize}
%Mapeamento adotado:
No mapeamento adodotado, a princípio mapeia-se em em uma relação 1:1 os primeiros 512MB de memória virtual para os 512MB de memória física disponíveis, ou seja, o endereço virtual será o mesmo que o endereço físico. Entretanto note que com o uso de \verb+Chunks+ e \verb+Address_space+ é possível alocar uma porção de memória em um mapeamento próprio, como explicado na seção \ref{sec:gerenciamento}.
Suponha que a MMU esteja ativa, e deseja-se salvar determinada informação numa determinada posição de memória física P, cujo endereço virtual correspondente seja V. Se apontarmos um ponteiro para a posição P, e escrever nesta posição, a MMU automaticamente irá interpretar P como sendo memória virtual, e irá traduzir P para uma outra posição de memória física qualquer e imprevisível.
Agora suponha que o mapeamento de memória esteja dividido no meio, de tal modo que a segunda metade faz uma relação 1:1 com a memória física. Deste modo, dado um endereço físico P, é possível calcular qual é seu endereço virtual V.
No EPOS foi feito um mapeamento semelhante, o intervalo de memória virtual [0x20000000 : 0x3FFFFFFF] mapeia para [0x0 : 0x1FFFFFFF]. Portanto \verb+P | 0x20000000 == V+, onde | é o operador \emph{or} de bits. Veja o seguinte exemplo de código:
\begin{lstlisting}
static Log_Addr phy2log(Phy_Addr phy){
return phy | PHY_MEM;
}
static Phy_Addr calloc(unsigned int frames = 1) {
Phy_Addr phy = alloc(frames);
memset(phy2log(phy), 0, sizeof(Frame) * frames);
return phy;
}
\end{lstlisting}
\fig{0.6}{memory_map}{Mapeamento de memória adotado. Os primeiros 512MB de memória virtual são mapeados 1:1 para a memória física, enquanto os 512MB de memória virtual seguintes mapeiam para os os mesmos 512MB de memória física. De 1GB à 4GB é um mapeamento simples 1:1, referentes aos registradores mapeados em memória.}
\verb+memset+ irá tentar escrever no endereço enviado como parâmetro, que será interpretado como um endereço virtual, que então será traduzido para um físico correspondente. Entretanto no lugar de um endereço físico, é enviado o endereço lógico no lugar, para que este seja então traduzido para o endereço físico que desejamos. Esta é uma maneira do SO internamente burlar a tradução da MMU.
O restante da memória virtual (1GB-4GB) é traduzido numa relação 1:1. Esta região de memória corresponde principalmente a registradores mapeados em memória, não sendo, portanto, memória RAM. A figura \ref{fig:memory_map} ilustra o mapeamento adotado.
Note que num mapeamento completo 1:1, a TTP1 tomaria $4\times4096$ bytes (4KB), e as 4096 TTP2 (uma para cada posição da TTP1) tomariam $4\times256\times4096$ bytes (4MB). Entretanto como o mapeamento virtual do intervalo 512MB-1GB pode usar as mesmas tabelas TTP2 que foram criadas com o mapeamento do intervalo 0-512MB, pode-se economizar $4\times256\times512$ bytes. Portanto, no total, as tabelas de tradução de páginas usadas pela MMU usam 3600KB de memória RAM.
%Realização deste mapeamento no código:
Em um mapeamento 1 para 1, para popular as TTPs, primeiramente, em um loop, itera-se 512 vezes (já que há 512 de RAM), salvando em posições sucessivas de uma determinada região da memória os endereços das TTP2s, levando-se em conta as \emph{flags} acima mencionadas, após as 512 posições, itera-se pelas por mais 512 posições, mapeando novamente os primeiros 512 MB de memória física. Após isto, mapeia-se as posições restantes em relação 1:1. Feito isto, é necessário também popular cada TTP2 que foi apontada pela TTP1, em um processo semelhante. Em um mapeamento por demanda, marca-se as regiões da memória virtual como não mapeadas, com a diferença que, ao invés de apenas abortar o acesso, cria-se dinamicamente aquele mapeamento.
Após feita as tabelas, a MMU está pronta para ser ativada. A MMU é controlada pelo coprocessador CP15, é nele que são guardados os registradores de configuração, endereço base para a TTP, dentre outras funções. Escrevendo-se 1 no bit menos significativo no registrador c1 (SCTLR) deste coprocessador, a MMU é ativada.
\section{Mediador da CPU}
O mediador da CPU encapsula uma série de funções/rotinas usadas pelo sistema. A maior parte destas funções precisam ser escritas diretamente em assembly, ou usam informações dependente de arquitetura. Este mediador não precisa ser instanciado nem inicializado, ele é apenas um conjunto de funções necessárias por outros componentes.
Entre as funções deste mediador estão:
\begin{itemize}
\item Ativar/desativar máscara de interrupções de um dado processador.
\item Rotinas para desligar, suspender e reiniciar o sistema.
\item Função usada pelo escalonador para fazer a troca de contexto.
\item Salvar/alterar registrador que guarda o endereço base da tabela de páginas.
\item Funções primitivas usadas em semáforos e mutexes, como tsl (\emph{test and set lock}), finc e fdec.
\item Inicializar CPU 1.
\item Funções para escrita/leitura de registradores.
\end{itemize}
Nesta classe também há uma classe interna chamada \verb+Context+. Ela possui funções para salvar e carregar o contexto do processador, e atributos para salvar cada registrador de propósito geral da arquitetura (incluindo sp e pc e cpsr). Esta classe é usada no momento da criação de uma \emph{thread}, e durante a troca de contexto.
%\section{Mediador SCU}
\section{Multicore}
Ao se ligar a placa, apenas a CPU0 executa código, e é função dela fazer as inicializações necessárias para então ligar o segundo processador (CPU1). Inicialmente, a CPU1 executa uma instrução (\verb+wfe+) que o coloca em modo de espera por evento (\emph{Wait for Event Mode}). Para sair deste modo, a CPU0 precisa emitir uma instrução do tipo evento de sistema, \verb+sev+ (\emph{System Event}). Quando a CPU1 recebe este sinal, ele imediatamente executa a instrução que está no endereço 0xfffffff0, localização onde a CPU0 deve previamente ter escrito uma instrução que faça a CPU1 fazer um \verb+jump+ para o ponto de entrada do que se deseja executar com a CPU1. Note que a posição 0xfffffff0 é uma região reservada da memória, e não corresponde à uma posição da RAM (é o mesmo princípio de registradores mapeados em memória).
Isto é suficiente para fazer o segundo processador passar a executar código, entretanto esta é a parte simples, ter dois processadores rodando tem várias implicações sobre como deve ser o \emph{design} de cada componente do sistema, pois agora há a preocupação com coerência e consistência de memória, condições de corrida {\emph{racing conditions}) e etc.
Um exemplo de componente que deve que ser adaptado para levar em conta que há mais de um processador, está no código da página \pageref{int_handler}, pois apenas o CPU0 pode escalonar processos no \emph{design} escolhido. Também é necessário levar em conta o fato de que agora é necessário invalidar a cache privada da CPU1 também, bem como criar uma pilha própria para a CPU1, assim como ativar seu \emph{timer} privado. Através do coprocessador que controla a MMU, é possível de configurar políticas de coerência de memória, como citado na seção \ref{sec:mmu}.
\fig{0.45}{cpu1_init}{Ponto de entrada da CPU 1 durante a inicialização.}
É necessário também definir um ponto de entrada para o segundo processador.
Foi escolhido iniciar o segundo processador já desde os primeiros estágios da inicialização do sistema, como é ilustrado na figura \ref{fig:cpu1_init}. Dentro de classes como \verb+Init_System+, onde são inicializado os mediadores do sistema, há desvios de fluxo (\verb=if's=) para inicializar apenas o que for pertinente para cada processador inicializar. Por exemplo, o \emph{timer} pode ser inicializado para cada \emph{core}, já a MMU não precisa ser inicializada duas vezes.
\subsection{Simetria}
Há duas estratégias comuns de lidar com multicores: Simétrica e assimétrica. Abaixo é definido o significado destes conceitos.\\[10pt]
%\begin{description}
% \item[Simétrico:]
\textbf{Simétrico:}
\begin{itemize}
\item Cada processador tem as mesmas funções.
\item Único SO para todos os processadores.
\item Qualquer processador pode executar tanto a aplicação quanto o SO.
\item Processadores precisam ser fisicamente idênticos.
\end{itemize}
% \item[Assimétrico:]
\textbf{Assimétrico:}
\begin{itemize}
\item Cada processador possui uma função específica
\item Possibilidade de executar mais de um SO no sistema (no máximo 1 por processador).
\item SO é executado por um único processador (ou subconjunto de processadores).
\item Pode ser implementado em arquiteturas com processadores idênticos ou diferentes.
\end{itemize}
%\end{description}
Note que os itens de cada conceito são flexíveis, podendo haver mistura de alguns itens.
Designs assimétricos costumam ser empregados em arquiteturas que possuem processadores difererentes em sua arquitetura, geralmente um deles sendo de menor poder de processamento e consumo de energia, enquanto o outro processador, de melhor desempenho, executa tarefas que exigem maior poder de processamento. Entretanto mesmo com processadores idênticos (no nosso caso, ambos \emph{cores} da Zedboard são idênticos), é possível de designar tarefas diferentes para cada processador, algo bastante usado em computação distribuída.
Na implementação feita, foi escolhido do design simétrico, significando que o EPOS é executado por todos os processadores, assim como a aplicação. O EPOS já possuia certo suporte para isto, pois ele ha havia sido portado para um IA32 \emph{multicore}, implementado de forma simétrica.
A decisão sobre qual é o melhor design depende muito do tipo da aplicação, e para as aplicações atuais do EPOS, o modelo simétrico é adequado.
\section{Testes}
Os testes foram feitos em dois ambientes: Simulação com o virtualizador de sistemas operacionais \verb=qemu=, e com uma Zedboard física.
\textbf{MMU: }
Como explicado na seção \ref{sec:map}, e ilustrado na figura \ref{fig:memory_map}, os primeiros 512 MB do espaço de endereçamento virtual são mapeados numa relação 1:1 com a memória física, e então os 512 MB de memória seguintes são mapeados também para os primeiros 512 MB de memória física.
Logo, se a MMU estiver ativa, o endereço virtual X (para X $\in [0..512MB]$) e X+512 MB apontam para a mesma posição de memória física.
Portanto, o teste consistiu em, antes de ligar a MMU, escrever um número numa posição de memória X, e então ler as posições X e X+512MB, e notar que evidentemente estas posições possuem valores diferentes. Então a MMU é ligada, e é lido novamente as posições X e X+512 MB, resultando agora no mesmo número, mostrando que a MMU esta ativa e traduzindo corretamente os endereços de memória virtuais.
\textbf{UART: }
A UART funcionou sem problemas no \verb=qemu=. Na placa, por via de um cabo usb, leu-se os dados pelo \verb=cutecom=. Os caracteres são enviados com sucesso pelo cabo serial. Pode ocorrer a perda de alguns caracteres dependendo da taxa de transmissão escolhida.
\textbf{GIC: }
No porte do GIC, o \verb=qemu= mostrou diferentes comportamentos quando usado diferentes versões, fazendo com que certas configurações do GIC funcionassem apenas em algumas versões. Foi possível analisar o comportamento das exceções e dos seus tratadores, entretanto as interrupções não pareciam estar sendo geradas nos intervalos corretos configurados no \verb=timer=, o que levou à conclusão que este componente só pode ser validado com precisão na placa física. Entretanto o GIC se mostrou não responsível na Zedboard, independente da configuração tentada. Foi feito um relatório a respeito, onde é exposto como, dentro das instruções do manual, este comportamento não pode corresponder ao esperado de acordo com o manual, indicando a possibilidade dele estar incompleto. Será necessário analisar outros códigos que usam a Zedboard.
%timer
\textbf{Timer: }
O \verb=timer= foi validado usando um \verb=Chronometer=, para contar por uma certa quantidade de tempo, onde após isto foi lido quanto tempo passou. Devido aos problemas descritos acima, na placa física o \verb+timer+ foi testado lendo-se o registrador mapeado em memória referente ao atual número que está sendo contado no \verb+timer+, onde constatou-se que ele de fato está operante e fazendo suas contagens de forma periódica.
%multicore
\textbf{Multicore: }
No \verb+qemu+ o segundo core foi ligado de forma bem sucedida, sincronizando sua inicialização nas barreiras que já existiam no EPOS com o primeiro \emph{core}, inicializando apenas os mediadores que eram necessários, e finalmente criando e executando suas \emph{threads} (thread \verb+main+ para o processador 0, e uma \verb=idle= no caso do processador 1). Como o segundo \emph{core} é ativado através de uma interrupção vinda da instrução \verb+sev+, as dificuldades com o GIC impediram a análise do comportamento na placa.
%cpu