UNIVERSIDADE DO VALE DO ITAJAÍ
CENTRO DE CIÊNCIAS TECNOLÓGICAS DA TERRA E DO MAR
CURSO DE CIÊNCIA DA COMPUTAÇÃO
PIQUENO
UM SISTEMA OPERACIONAL EDUCACIONAL PARA O RASPBERRY PI
por
Daniel Santos Bathke
Itajaí (SC), novembro de 2013
UNIVERSIDADE DO VALE DO ITAJAÍ
CENTRO DE CIÊNCIAS TECNOLÓGICAS DA TERRA E DO MAR
CURSO DE CIÊNCIA DA COMPUTAÇÃO
PIQUENO
UM SISTEMA OPERACIONAL EDUCACIONAL PARA O RASPBERRY PI
Área de Sistemas Operacionais
por
Daniel Santos Bathke
Relatório apresentado à Banca Examinadora
do Trabalho Técnico-científico de Conclusão
do Curso de Ciência da Computação para
análise e aprovação.
Orientador: Fabrício Bortoluzzi, M.Sc.
Itajaí (SC), novembro de 2013
Dedico este trabalho ao meu avô materno, Lúcio dos Santos, que sempre foi e ainda é
referência familiar nos quesitos de comportamento. Este pescador da Praia Alegre, Penha, que
sempre deu muito valor a sua família, era chamado de Quininho.
Presto uma homenagem a seu nome ao batizar o PIqueno, derivado de piqueno,
piquininho e quininho.
RESUMO
BATHKE, Daniel S. PIqueno: Um sistema operacional educacional para o Raspberry Pi.
Itajaí, 2013. 111. Trabalho Técnico-científico de Conclusão de Curso (Graduação em Ciência
da Computação) – Centro de Ciências Tecnológicas da Terra e do Mar, Universidade do Vale
do Itajaí, Itajaí, 2013.
Este trabalho documenta a criação de uma ferramenta educacional para ser utilizada em sala
de aula na disciplina de sistemas operacionais, nos cursos da Ciência da Computação e
Engenharia da Computação da Universidade do Vale do Itajaí (UNIVALI). Acredita-se que
há uma lacuna entre a teoria e a prática no ensino desta disciplina, quando o aluno sai de
outras disciplinas com atividades mais práticas e entra nesta, onde são apresentados vários
conceitos teóricos, mas sem exercita-los e conecta-los aos conhecimentos previamente
obtidos. Trata-se do PIqueno, um sistema operacional simples o suficiente para ser
compreendido e estudado, para o Raspberry Pi, uma plataforma simples e barata desenvolvida
também para propósitos eduacionais. Desenvolvido em Assembly e C, o PIqueno abre mão de
otimizações de código-fonte para ganhar em clareza de entendimento. Espera-se que o
PIqueno possa contribuir para aumentar a qualidade do ensino, servindo como ferramenta
para os professores de sistemas operacionais que utilizam metodologias de ensino com base
construtivista.
Palavras-chave: Sistemas Operacionais. Raspberry Pi. Educação.
ABSTRACT
This paper documents the creation of an educational tool to be used in the classroom in the
discipline of operating systems of Computer Science and Computer Engineering courses at
Universidade do Vale do Itajaí (UNIVALI). It is believed that there is a gap between theory
and practice in teaching this subject, when the student leaves other disciplines with more
practical activities and enters this, which presents various theoretical concepts, but without
exercising them and connecting them to the previously obtained knowledge. It's PIqueno, an
operating system simple enough to be understood and studied, for the Raspberry Pi, a
platform developed to be simple and inexpensive also for educational purposes. Developed in
C and Assembly, PIqueno waive optimizations source to gain in clarity of understanding. It is
expected that PIqueno might contribute to increasing the quality of education, serving as a
tool for teachers of operating systems that use constructivist based teaching methods.
Keywords: Operating System. Raspberry Pi. Education.
LISTA DE FIGURAS
Figura 1. O Raspberry Pi .......................................................................................................... 20
Figura 2. Ilustração de componentes ........................................................................................ 22
Figura 3. (a) A multiprogramação, (b) contadores de 4 processos e (c) somente 1 processo
executando por vez ................................................................................................................... 24
Figura 4. Hierarquia de processos ............................................................................................ 25
Figura 5. Estados de processos ................................................................................................. 27
Figura 6. Ilustração de vetores de interrupção .......................................................................... 30
Figura 7. Alternância do processador para tratar interrupções ................................................. 31
Figura 8. Fluxograma de mudanças do modo do processador ................................................. 32
Figura 9. Ilustração da organização da memória principal ....................................................... 33
Figura 10. Ilustração de execução de processos usando FIFO ................................................. 36
Figura 11. Ilustração de execução de processos usando SJF.................................................... 37
Figura 12. Ilustração de execução de processos usando Round Robin .................................... 39
Figura 13. Diagrama de Casos de Uso ..................................................................................... 47
Figura 14. Diagrama de Domínio do PIqueno .......................................................................... 49
LISTA DE TABELAS
Tabela 1. Tabela comparativa de sistemas operacionais semelhantes e o PIqueno ................. 45
LISTA DE QUADROS
Quadro 1. Código de inicialização ........................................................................................... 50
Quadro 2. Procedimento Main.................................................................................................. 51
Quadro 3. Procedimento create_main_process ........................................................................ 52
Quadro 4. Definição da struct process ..................................................................................... 52
Quadro 5. Processo invocando uma Syscall para término ........................................................ 54
Quadro 6. Função interrupts_init configurando vetor de interrupções .................................... 55
Quadro 7. Tratamento de uma interrupção de IRQ invocando o escalonador ......................... 56
Quadro 8. Escalonador trocando processos do processador ..................................................... 58
LISTA DE ABREVIATURAS E SIGLAS
A
ARM
CPU
DHCP
FIFO
GPIO
GPU
HDMI
IDE
Mbps
RAM
RCA
RISC
ROM
SJF
SoC
TTC
UML
UNIVALI
USB
V
Ampere
Advanced RISC Machine
Central Processing Unit
Dinamic Host Configuration Protocol
First In First Out
General Purpose Input/Output
Graphic Processing Unit
High Definition Media Interface
Integrated Development Environment
Mega bits por segundo
Random Access Memory
Radio Corporation of America
Reduced Instruction Set Computer
Read Only Memory
Shortest Job First
System on Chip
Trabalho Técnico-científico de Conclusão de Curso
Unified Modeling Language
Universidade do Vale do Itajaí
Universal Serial Bus
Volt
SUMÁRIO
1 INTRODUÇÃO .................................................................................................................. 15
1.1 PROBLEMATIZAÇÃO..................................................................................................... 16
1.2 FORMULAÇÃO DO PROBLEMA .................................................................................. 16
1.2.1 Solução Desenvolvida ................................................................................................... 17
1.3 OBJETIVOS ..................................................................................................................... 17
1.3.1 Objetivo Geral .............................................................................................................. 17
1.3.2 Objetivos Específicos .................................................................................................... 17
1.4 METODOLOGIA ............................................................................................................. 18
1.5 ESTRUTURA DO TRABALHO ........................................................................................ 19
2 FUNDAMENTAÇÃO TEÓRICA ..................................................................................... 20
2.1 RASPBERRY PI ............................................................................................................... 20
2.1.1 Especificações técnica .................................................................................................. 21
2.2 SISTEMAS OPERACIONAIS ........................................................................................... 22
2.2.1 Processo ........................................................................................................................ 23
2.3 CONSTRUTIVISMO ........................................................................................................ 40
3 DESENVOLVIMENTO .................................................................................................... 45
3.1 DIAGRAMA DE CASOS DE USO ................................................................................... 47
3.2 REQUISITOS ................................................................................................................... 48
3.2.1 Requisitos não funcionais ............................................................................................. 48
3.2.2 Requisitos funcionais .................................................................................................... 48
3.3 DIAGRAMA DE DOMÍNIO............................................................................................. 48
3.4 CENÁRIO DE UTILIZAÇÃO EM SALA DE AULA ........................................................ 58
4 CONSIDERAÇÕES FINAIS ............................................................................................ 60
4.1 TRABALHOS FUTUROS ................................................................................................. 60
4.1.1 Drivers para dispositivos de E/S .................................................................................. 61
4.1.2 Sistema de arquivos ...................................................................................................... 61
4.1.3 Gerenciamento de E/S .................................................................................................. 61
4.1.4 Definição de arquivo binário ........................................................................................ 62
4.1.5 Compilador para o PIqueno ......................................................................................... 62
4.1.6 Gerenciamento de memória .......................................................................................... 62
4.1.7 Interface de linha de comando...................................................................................... 63
4.1.8 Driver para dispositivo de rede .................................................................................... 63
4.1.9 Conjunto de ferramentas para execução em linha de comando ................................... 64
4.1.10 Gerenciamento de usuários ........................................................................................ 64
4.1.11 Permissões .................................................................................................................. 64
4.1.12 Interface gráfica ......................................................................................................... 65
APÊNDICE A. CÓDIGO FONTE ....................................................................................... 67
15
1 INTRODUÇÃO
Atualmente, um hardware sozinho não é capaz de fazer muita coisa, mas, em conjunto
com um software pode executar tarefas rotineiras como acessar uma página na internet,
escutar uma música, assistir a um vídeo ou tarefas grandiosas como fazer cálculos de previsão
de desastres, simulação de ambientes biológicos, entre outras.
Esse poder e flexibilidade alcançados devem-se grande parte ao software que
conceitualmente é chamado de Sistema Operacional. Este veio para resolver dois problemas
principais: (i) dificuldade na programação de aplicativos e (ii) falta de um gerenciador de
recursos.
Ao se construir softwares aplicativos para o usuário, exigia-se que o programador
conhecesse cada tipo de hardware que fosse usado e que tratasse cada uma de suas
particularidades para que, desta forma, pudesse utilizá-los em favor do usuário. Essa
exigência tornava muito custoso o desenvolvimento de um software aplicativo e, praticamente
nulo o reaproveitamento de código de um hardware para outro. Com isso, viu-se a
necessidade de abstrair a programação de hardware em partes específicas, feias e difíceis de
programar para uma camada de nível maior, com chamadas comuns e simples de usar.
Entende-se esse conceito por máquina estendida (TANENBAUM, 2010).
Pensando no aumento de poder de computação de um hardware, viu-se a necessidade
de este executar mais tarefas ao mesmo tempo, pois o que se tinha até dado momento era um
hardware específico para cada tarefa. Entende-se que um processador, por exemplo, é rápido
suficiente para executar muito mais do que uma tarefa, desde que este destinasse uma parte do
seu tempo a cada tarefa em execução. Dentro desse cenário, duas tarefas poderiam tentar
utilizar a impressora ao mesmo tempo, o que poderia ser desastroso e arruinar o resultado de
ambas no ponto de vista do usuário. Para resolver este problema, criou-se um conjunto de
software que fizesse essa organização de tal forma que, em um caso como este cada tarefa
imprimisse seu trabalho até o fim sem ser interrompida. Denomina-se esta solução como
gerenciador de recursos (TANENBAUM, 2010).
A combinação desses dois conceitos forma o que se conhece hoje como Sistema
Operacional, provendo uma máquina estendida fácil de programar e gerenciando os recursos
de hardware a fim de executar as tarefas do usuário com sucesso.
16
Por se tratar desse nível de complexidade e extensão, muitos professores de sistemas
operacionais consideram que a melhor maneira de aprendizado dos alunos é a interação com o
mundo real, através de problemas concretos. Isso se traduz em explorar o núcleo de um
sistema operacional com código-fonte disponível (MAZIERO, 2002).
Porém, a didática do contato direto pode causar diversos problemas. Entre eles estão o
excessivo número de linhas de código-fonte em um sistema operacional real, como o caso do
Linux que possui 3,3 milhões ou o do próprio MINIX, com 35 mil, a dificuldade de
compreensão das otimizações, a dificuldade em depuração ou no ciclo de desenvolvimento no
interior do kernel (núcleo) e, por fim, um custo elevado para disponibilizar computadores
exclusivos para este fim (MAZIERO, 2002).
Este trabalho técnico-científico se propôs a desenvolver o PIqueno, um sistema
operacional para o computador Raspberry Pi com foco no aprendizado de alguns conceitos,
abrindo mão de complexidade e otimizações para ganhar legibilidade no código-fonte. O
objetivo é que estudantes da disciplina de sistemas operacionais com duração de apenas um
semestre tenham como possibilidade o estudo prático de um sistema operacional simples, em
complemento ao método tradicional baseado na interpretação de conceitos.
O Raspberry Pi é um minicomputador de uso geral, pequeno o bastante para caber em
um bolso e relativamente barato, custando cerca de 35 dólares no Reino Unido e 150 reais no
Brasil (em meados de maio de 2013). Depois de comprado pode-se utiliza-lo com monitores
ou televisores convencionais, teclado e mouse. Foi feito para ser inicializado do cartão de
memória com um sistema operacional de código-fonte aberto, facilitando crianças a
programarem e tentarem coisas novas sem se preocupar em estragar o computador da família
ou com o tamanho do prejuízo caso algo dê errado (HALFACREE; UPTON, 2012).
1.1 PROBLEMATIZAÇÃO
1.2 FORMULAÇÃO DO PROBLEMA
As disciplinas de sistemas operacionais ministradas nos cursos de graduação na área
de comptuação na UNIVALI abordam os mesmos conceitos e sofrem das mesmas carências
que estas disciplinas enfrentam nas demais universidades: um curto período de tempo para
tratar de tantos conceitos fundamentais do assunto, como o escalonamento de processos, o
17
chaveamento de contextos, o funcionamento das interrupções, a proteção da memória
principal, impasses e diversos outros.
Ao optar por abordar tais temas também sob a forma de experimentos práticos, carece
o professor muitas vezes de um conjunto de experimentos que sejam confiáveis, realistas,
corretos e principalmente: simplistas e livres de complexidades desnecessárias que poderiam
causar uma curva de aprendizado complexa e inviável.
Acredita-se que se poderia obter resultados melhores de aprendizagem utilizando de
recursos mais práticos, fazendo uso de estratégias fundamentadas no construtivismo, como
trabalho em grupo e interação entre o aluno e o objeto.
1.2.1 Solução Desenvolvida
Este trabalho propõe o desenvolvimento e uso do PIqueno, um sistema operacional
educacional para o Raspberry Pi, com foco na legibilidade do código-fonte e recursos
limitados para ganhar simplicidade e tornar o processo de aprendizagem prática mais rápida.
O mesmo limita-se, a princípio, a exemplificar e permitir explorações do conceito dos
conceitos de escalonamento de processos e de seu mecanismo de chaveamento de contexto
subjacente. Como consequência, os conceitos de (i) modo real e protegido e (ii)
escalonamento de processos, também são abordados.
1.3 OBJETIVOS
1.3.1 Objetivo Geral
O objetivo geral deste trabalho foi desenvolver e documentar o PIqueno, de forma que
o mesmo possa ser utilizado como ferramenta de apoio no ensino da disciplina de sistemas
operacionais do curso de Ciência da Computação da UNIVALI.
1.3.2 Objetivos Específicos
O objetivo geral deste trabalho foi alcançado através do cumprimento dos objetivos
específicos a seguir:

Compreender a arquitetura do Raspberry, seu funcionamento interno e planejar sua
inicialização;
18

Pesquisar sistemas operacionais de porte e propósito similar ao ansiado por este
trabalho;

Modelar o PIqueno alinhado às expectativas de finalidade de apoio ao ensino. Os
artefatos produzidos nesta etapa foram: (i) análise de requisitos funcionais e não
funcionais, (ii) diagrama de casos de uso e (iii) diagrama de domínio;

Estudar estratégias pedagógicas para aplicação do PIqueno em sala de aula;

Executar a implementação do PIqueno;

Documentar o TTC em formato de um artigo de iniciação científica e publicar seu
código-fonte em um repositório de acesso público.
1.4 METODOLOGIA
A metodologia deste trabalho foi dividida em seis partes, cada uma para alcançar
diretamente um objetivo específico para que, ao final, o objetivo geral também fosse
alcançado.

Revisão: Nesta etapa foi feita uma revisão sobre os conceitos de sistemas
operacionais, para que fosse definido os que o PIqueno deveria apoiar. Também
foi feita uma análise sobre a arquitetura do Raspberry Pi e suas características,
gerando uma documentação para apoio na implementação;

Análise de similares: Foi feito uma pesquisa sobre os sistemas operacionais
semelhantes ao PIqueno, em tamanho e propósito. Com essa pesquisa é possível
gerar um quadro comparando as características de cada um, a fim de conhecer
onde cada um se diferia;

Modelagem: Foram elaborados diagramas de caso de uso e de domínio e elicitados
os requisitos funcionais e não-funcionais, de acordo com a UML, que o PIqueno
deveria atender para alcançar seu propósito;

Estudo pedagógico: Estratégias pedagógicas foram estudadas para proporcionar
melhores aplicações do PIqueno em sala de aula;
19

Implementação: Etapa onde o PIqueno foi própriamente feito. Foram utilizadas as
linguagens Assembly e C, compiladas para ARM em um PC x86 com a toolchain
gcc-arm-linux-gnueabihf, disponível para Linux;
1.5 ESTRUTURA DO TRABALHO
Este trabalho está dividido em 3 partes: (i) introdução, (ii) fundamentação e (iii)
projeto.
Na introdução é exposta a problemática que tangiu a direção deste trabalho e a solução
desenvolvida. A problemática baseia-se no artigo de MAZIERO (2002), onde afirma existir
uma deficiência nas ferramentas disponíveis para o ensino da disciplina de sistemas
operacionais.
A fundamentação teórica recupera os conceitos-chave de sistemas operacionais,
introduz o Raspberry Pi e trás a metodologia pedagógica que se pretende alcançar com o
desenvolvimento e publicação do PIqueno.
O desenvolvimento descreve como a parte prática da implementação do PIqueno foi
executada, bem como sua documentação, requisitos funcionais e não funcionais e diagrama de
domínio.
20
2 FUNDAMENTAÇÃO TEÓRICA
A fundamentação teórica deste trabalho está dividida em três seções. A primeira seção,
2.1, será destinada a definir e descrever o Raspberry Pi. A seção 2.2 descreve os conceitos de
sistemas operacionais com especial atenção àqueles que foram empregados no PIqueno. A
seção 2.3 trata das metodologias de ensino que justificaram a elaboração deste trabalho.
2.1 RASPBERRY PI
Raspberry Pi é um minicomputador do tamanho de um cartão de crédito. Este foi feito
por um grupo de pesquisadores (entre eles Eben Upton, principal idealizador) com o propósito
de estimular crianças a aprenderem a programar. (HALFACREE; UPTON, 2012).
Fruto de uma longa pesquisa, este teve sua maior oportunidade de realização depois de
2008, quando os processadores projetados para dispositivos móveis começaram a ficar mais
acessíveis. Nessa época, Upton trabalhava na Broadcom como arquiteto de chip (Ibidem).
A Figura 1 mostra o Raspberry Pi.
Figura 1. O Raspberry Pi
21
Fonte: Adaptado de http://www.raspberrypi.org/faqs (2013).
2.1.1 Especificações técnica
Sua arquitetura consiste de um SoC (System on Chip, sistema em chip), equipado com
um processador de multimídia da família ARM (Advanced RISC Machine, máquina RISC
avançada), GPU (Graphic Processor Unit, unidade de processamento gráfico) e controlador
de áudio integrado. Trata-se do chip BCM2835, fabricado pela Broadcom (Ibidem).
Seu processador é a chave para juntar-se baixo custo, simplicidade, bom poder gráfico
e baixo consumo de energia. Ele possibilita que o Raspberry Pi seja alimentado com apenas
5V e consuma 1A, através de uma porta no padrão micro USB (Universal Serial Bus,
Barramento Serial Universal) (Ibidem).
O minicomputador ainda conta com um controlador USB, controlador de rede
Ethernet 10/100Mbps, 512MB de memória RAM (Random Access Memory, Memória de
Acesso Randômico) e um leitor de cartão de memória. Seus meios de entrada e saída contam
com 2 portas USB, 1 porta HDMI (High Definition Multimedia Interface, Interface de mídia
de alta definição), 1 porta RCA (proveniente do nome da fabricante Radio Corporation of
America) e um conjunto de pinos GPIO (General Purpose Input/Output, entrada e saída de
propósito genérico) (Ibidem).
A Figura 2 mostra uma ilustração dos componentes do Raspberry Pi.
22
Figura 2. Ilustração de componentes
Fonte: Adaptado de http://www.raspberrypi.org/faqs (2013).
O alinhamento entre os propósitos educacionais do PIqueno e do Raspberry Pi foram
decisivos para a sua escolha. A simplicidade e custo baixo do Raspberry Pi aumentam as
chances do PIqueno ser utilizado em salas de aula, laboratórios ou até mesmo ser adquirido
por alunos.
2.2 SISTEMAS OPERACIONAIS
Um sistema operacional pode ser visto sob duas óticas: (i) como máquina estendida ou
(ii) como gerenciador de recursos. Como máquina estendida, faz a abstração do hardware para
o programador de aplicativo. Como gerenciador de recursos, ele organiza o hardware de
maneira que cada aplicativo possa utiliza-lo de maneira justa e coerente (TANENBAUM,
2010).
23
Essa técnica presente nos sistemas operacionais modernos foi resultado de uma
mudança no paradigma da computação, em que o computador executava uma tarefa por vez e
passou a executar várias coisas ao mesmo tempo, pelo menos na percepção do usuário. Essa
percepção de simultaneidade ou de paralelismo chama-se pseudoparalelismo: a alternância da
CPU entre programas é tão rápida que dá ao usuário a falsa impressão de executar todos os
programas ao mesmo tempo.
Com a alternância da CPU entre programas criou-se um problema: a concorrência por
recursos. Agora, quando um programa executa, ele não pode mais assumir controle total do
computador. Foi para resolver este problema que foi criado o conceito de gerenciador de
recursos, para que o usuário possa executar mais tarefas ao mesmo tempo sem que estas
atrapalhem o resultado uma das outras, tornando-se assim inúteis. O programador de
aplicativo não deve mais assumir o controle total do hardware: de fato nem o controlar, mas
solicitar ao sistema operacional para que este execute de forma organizada e padronizada
algum procedimento.
Em outra perspectiva, que visa facilitar e impulsionar o desenvolvimento de
aplicativos, um sistema operacional pode ser visto como máquina estendida, criando uma
interface simplificada e padronizada de acesso ao hardware pelo programador de aplicativo.
Com isso, os mesmos deixaram de se preocupar, por exemplo, com a velocidade do disco
rígido, qual trilha a agulha disco rígido se encontra e apenas se preocupam em solicitar ao
sistema operacional que abra um arquivo e retorne seu conteúdo.
Feita essa revisão, é possível aprofundar-se no estudo de sistemas operacionais,
começando pelo conceito mais central de todo sistema operacional (TANENBAUM, 2010), e
que será o foco inicial de aprendizagem proporcionada pelo PIqueno, o processo.
2.2.1 Processo
Para que vários programas possam compartilhar o processador, faz-se necessário uma
troca ou alternância entre eles, até que os mesmos sejam finalizados. Como o processador só
consegue executar um processo de cada vez, é dever do sistema operacional determinar
quando e como eles serão alternados. Para que um programa seja interrompido pela metade
para ceder tempo de processador a outro programa, seu estado atual deve ser armazenado,
para que posteriormente possa ter seus dados recuperados e ter sua execução continuada da
parte onde parou.
24
O programa em execução, suas variáveis, o contador de programa e seus registradores
formam o conceito de processo (TANENBAUM, 2010). O sistema operacional deve
organizar os processos, de forma que possa priorizar e executar todos da forma mais
conveniente para o usuário.
Naturalmente, o primeiro processo a ser executado é o processo de inicialização, onde
o conjunto de softwares do sistema operacional é carregado, dando posteriormente ao usuário
um meio de solicitar a execução de outros processos de seu interesse.
A Figura 3 mostra
Figura 3. (a) A multiprogramação, (b) contadores de 4 processos e (c) somente 1 processo
executando por vez
Fonte: Adaptado de TANENBAUM (2010).
2.2.1.1
Relacionamento entre processos
Na maioria dos sistemas operacionais, os processos são criados por outros processos
através de uma chamada de sistema, gerando assim uma hierarquia de processos. Quando o
processo de inicialização termina, ele cria alguns processos, como por exemplo, uma shell
(interface de linha de comando) ou gerenciador gráfico, que por sua vez pode iniciar o
antivírus, o gerenciador de conexões de rede, entre outros. Neste ponto, o próprio usuário
pode selecionar quais programas quer utilizar, gerando assim um processo para cada um.
Percebe-se então o conceito de hierarquia, chamada de árvore de processos, onde cada
processo tem um pai e nenhum ou muitos filhos (OLIVEIRA; TOSCANI; CARISSIMI,
2010).
25
A Figura 4 ilustra um possível estado da árvore de processos derivado do exemplo
anterior, sendo P1 o processo de inicialização, P2 o gerenciador gráfico, P5 antivírus, P6 o
gerenciador de conexões de rede e P7 algum programa executado pelo usuário.
Figura 4. Hierarquia de processos
Fonte: Adaptado de OLIVEIRA; TOSCANI; CARISSIMI (2010).
Quando um processo termina ou é encerrado pelo usuário, o tronco de processos que
deriva deste tem de ser tratado. Cada sistema operacional tem sua maneira de lidar com essa
situação, podendo (i) destruir todos os processos filhos do processo terminado, (ii) manter
como referência o processo encerrado na árvore de processos até que todos seus filhos tenham
sido encerrados ou ainda (iii) vincular todos os processos filhos ao seu processo “avô”
(OLIVEIRA; TOSCANI; CARISSIMI, 2010).
2.2.1.2
Estados de um processo
Sabe-se que um computador tem um número muito limitado de processador. Até a
década passada era mais comum que tivesse apenas um (1). Para simplificar este estudo,
considera-se o caso de apenas um processador por computador.
Seguindo, mesmo com um processador, um computador é capaz de executar diversos
processos. De fato, muitas partes de um sistema operacional executam como processos
separados entre si, talvez apenas com alguma troca de informação. O mesmo acontece com
programas aplicativos, normalmente eles dependem muito de outros recursos do computador
além do processador, ou seja, fazem muitas chamadas de sistema.
26
Quando um processo faz uma chamada de sistema, este tem de esperar o seu resultado
para poder seguir a execução do programa. Neste momento, não faz mais sentido que este
ocupe o processador. Inclusive, as próprias chamadas ao sistema dependem às vezes de
processador para serem executadas. Tem-se então a primeira necessidade de alternância de
processos no processador, para que outros processos dependentes possam ser executados a
fim de liberar o processo original a ter sua execução continuada (OLIVEIRA; TOSCANI;
CARISSIMI, 2010).
Outra situação é a necessidade de pseudoparalelismo que um sistema operacional deve
prover. Ou seja, um sistema operacional moderno tem de dar ao usuário a capacidade de
executar várias tarefas ao mesmo tempo, e quanto menos ele perceber a alternância de
processos, mais agradável fica sua experiência. Para tal, o sistema operacional tem que
impedir que um processo domine o processador até que este seja encerrado, pois dessa
maneira outras tarefas ficariam bloqueadas, inclusive a interface gráfica, dando a impressão
de travamento para o usuário.
Para atender esta necessidade, o sistema operacional conta com um módulo chamado
escalonador de processos, que basicamente enfileira os processos prontos para execução e
coloca-os para executar em fatias de tempo no processador. Essa organização e priorização
podem ser feita de várias maneiras e será abordada posteriormente.
A Figura 5 demonstra os cinco (5) estados de processos mais comuns entre os sistemas
operacionais atuais.
27
Figura 5. Estados de processos
Fonte: Adaptado de OLIVEIRA; TOSCANI; CARISSIMI (2010).
Quando uma chamada de sistema é feita para a criação de um processo, fork() (se
dividir) nos casos mais comuns, o sistema operacional faz uma cópia do processo original
insere na árvore de processos como seu filho e, daí para frente, carrega outro programa para
ser executado nele. Durante este procedimento, o processo filho fica em estado de (i) criação.
Após o término da criação do processo filho, o mesmo muda seu estado para (ii) apto.
Este estado não significa que o mesmo entrará em execução imediatamente. Apenas significa
que já pode entrar na fila do escalonador de processos para concorrer por um tempo no
processador (OLIVEIRA; TOSCANI; CARISSIMI, 2010).
Sendo selecionado para execução pelo escalonador, o processo vai para o estado (iii)
executando. Isso significa que todas suas variáveis, registradores e o contador de programa
foram carregados para o processador. A partir deste momento o processo encontra-se
efetivamente em execução dentro do processador (OLIVEIRA; TOSCANI; CARISSIMI,
2010).
Quando um processo tem de ser interrompido porque seu programa teve a necessidade
de alguma chamada de sistema, o mesmo vai para o estado (iv) bloqueado. Isso significa que
este processo já não tem mais o que executar no processador até que a chamada de sistema
realizada seja executada. Exemplo: Um programa solicita ao sistema operacional a leitura de
28
um arquivo. Até que o arquivo seja lido e retornado, o programa não tem mais como seguir
adiante em sua execução. Durante este estado, o processo não participa do rateio do
processador pelo escalonador de processos. Quando a chamada de sistema dependente for
executada, o mesmo volta para o estado (ii) apto. Existem algumas exceções para esta
mudança, como por exemplo quando a chamada de sistema é executada muito rapidamente.
Para este caso, o processo é autorizado a voltar diretamente para o estado (iii) em execução
(OLIVEIRA; TOSCANI; CARISSIMI, 2010).
A qualquer momento o sistema operacional, através do escalonador de processos, pode
interromper a execução de qualquer processo e eleger outro para entrar em execução. Quando
isso acontece, o processo interrompido sai do estado (iii) em execução para o estado (ii) apto,
e volta para a fila de rateio do processador (OLIVEIRA; TOSCANI; CARISSIMI, 2010).
Sempre quando um processo sai do estado (iii) em execução, o sistema operacional
tem de executar uma série de procedimentos para que o mesmo possa voltar em execução
mais tarde. Esses procedimentos consistem normalmente em salvar todos os dados
relacionados ao processo que estão no processador. Entre os dados estão o contador de
programa, todas as suas variáveis e registradores relacionados (OLIVEIRA; TOSCANI;
CARISSIMI, 2010).
Quando o processo está no estado (iii) em execução e seu programa termina de ser
executado, o mesmo vai para o estado (v) de destruição. Neste ponto, tudo que o programa
deveria fazer, já foi feito. Exemplo: no comando cat, no Linux, o programa lê um ou uma
série de arquivos e exibe ao usuário. Depois disto, ao entrar no estado (v) de destruição, o
sistema operacional pode a qualquer momento apagar todos os seus recursos, como
registradores e espaço em memória e liberá-los para serem utilizados por outros programas.
Após a conclusão deste passo, o processo já não constará na árvore de processos (OLIVEIRA;
TOSCANI; CARISSIMI, 2010).
Pode ser que um processo solicite uma chamada de sistema ilegal, causando erro. Para
este caso, após detectado o erro na chamada de sistema, o processo é passado do estado (iv)
bloqueado para o estado (v) de destruição, por motivo de erro (OLIVEIRA; TOSCANI;
CARISSIMI, 2010).
2.2.1.3
Interrupções
29
Toda alternância de processos no processador é feita por interrupções, seja ela de
hardware ou de software. As interrupções de hardware são geradas pelos controladores de
periféricos de entrada e saída (OLIVEIRA; TOSCANI; CARISSIMI, 2010). Quando o disco
rígido, por exemplo, termina de ler uma informação e quer disponibiliza-la para o
processador, o controlador do disco rígido gera uma interrupção de hardware, chamando a
atenção do processador para o fato de que os dados já estão prontos.
Outro exemplo importante é o teclado e mouse, que o usuário utiliza para se expressar
dentro dos aplicativos. O processador pode estar ocupado com algum processo, mas quando o
usuário mexe o mouse ou digita algo no teclado, o controlador específico desses hardwares
enviam para o processador uma interrupção informando este evento.
A interrupção de hardware é feita através do barramento de controle, com sinais
elétricos gerados pelo controlador do dispositivo, que chegam diretamente no processador.
Cada interrupção tem um tipo associado, pois cada uma precisa ser tratada de maneira
diferente, e uma prioridade. Tipicamente os tipos variam de 0 a 255, em que quanto mais
perto do zero, maior prioridade se tem. Ao tratar uma interrupção, é desabilitado
temporariamente o tratamento de qualquer interrupção com prioridade menor. Após o
tratamento de uma interrupção, o processador verifica por interrupções menos prioritárias
pendentes e depois habilita novamente o tratamento de qualquer interrupção (OLIVEIRA;
TOSCANI; CARISSIMI, 2010).
Quando o processador recebe uma interrupção de hardware, ele salta para um endereço
de rotina, encontrado no vetor de interrupções, associado ao tipo de interrupção recebida. Esse
endereço possui o endereço de uma rotina de software para tratar a interrupção, então o
processador interrompe a execução do processo que está executando, salva todos os seus
dados e executa a rotina. (OLIVEIRA; TOSCANI; CARISSIMI, 2010).
A Figura 6 exemplifica um vetor de interrupções, com endereços diferentes para
interrupções diferentes.
30
Figura 6. Ilustração de vetores de interrupção
Fonte: Adaptado de OLIVEIRA; TOSCANI; CARISSIMI (2010).
As interrupções via software, também chamadas de trap, são geradas quando um
programa precisa fazer uma chamada de sistema. De forma semelhante, cada interrupção de
software também possui um tipo e um endereço de rotina conhecido pelo sistema operacional.
Dessa forma, o programa não precisa conhecer quem irá tratar sua interrupção (OLIVEIRA;
TOSCANI; CARISSIMI, 2010).
A Figura 7 demonstra em uma linha de tempo a alternância do processador enquanto
executa um processo e trata duas interrupções.
31
Figura 7. Alternância do processador para tratar interrupções
Fonte: Adaptado de OLIVEIRA; TOSCANI; CARISSIMI (2010).
2.2.1.4
Modos de operação do processador
O sistema operacional é responsável por manter a proteção dos dispositivos, para que
tudo funcione corretamente. Para que isso aconteça, os processadores precisam dispor em sua
arquitetura um chaveamento de contexto, para alternar entre modo supervisor e modo usuário.
Quando o computador liga, o processador procura o código de inicialização do sistema
operacional em um local específico da memória, geralmente em ROM. Para carregar o
sistema operacional, o processador está no modo supervisor. Em seguida, o sistema
operacional executa o seu processo de inicialização, e sempre que executar um programa de
usuário, ele chaveia o processador para modo usuário.
No momento que o programa de usuário necessita de acesso a algum hardware ou
instrução privilegiada, ele faz uma chamada de sistema (através de uma interrupção de
software). Quando a interrupção é acionada, o processador muda para modo supervisor e
carrega o tratador dessa interrupção em modo supervisor, para poder atender a chamada do
usuário.
Desta maneira o núcleo do sistema operacional, também chamado de kernel e que
possui as partes que precisam de acesso direto ao hardware, sempre executa em modo
supervisor. Já os processos de usuário sempre executam em modo usuário, pois não têm
permissão para invocar diretamente o hardware. Quando algum processo de usuário tenta
32
executar instruções diretas a periféricos (as quais não tem privilégio), ocorre então uma
interrupção e o sistema operacional é carregado em modo supervisor para tratar o acesso
inválido.
A Figura 8 mostra um fluxograma com as possíveis maneiras de mudanças de modo
do processador.
Figura 8. Fluxograma de mudanças do modo do processador
Fonte: Adaptado de OLIVEIRA; TOSCANI; CARISSIMI (2010).
2.2.1.5
Proteção de memória
Da mesma maneira que o processador, a memória também precisa de proteção. Se um
programa de usuário conseguir acessar endereços de memória do sistema operacional, todo
seu funcionamento ficaria comprometido. O usuário poderia substituir uma rotina de
tratamento de alguma interrupção, por exemplo, e com isso executar instruções no
processador em modo supervisor.
Outro problema que motivou a criação do mecanismo de proteção de memória, é a
proteção de endereços de memória de um processo contra outro processo. Ou seja, o sistema
operacional deve garantir que endereços de memória do processo A seja unicamente acessado
e alterado por ele, e não pelo processo B. Isso também geraria problemas de funcionamento,
embora de escala menor a nível de hardware, mas definitivamente poderá ser devastador para
o usuário que depende do processo A.
A proteção da memória também necessita de uma ajuda da arquitetura de hardware. A
maneira clássica, porém funcional, de se fazer essa proteção é a utilização de registradores
33
limites. Antes de fazer o chaveamento do processador para modo usuário, o sistema
operacional carrega nos registradores-limite os endereços de limite inferior e superior de
memória do próximo processo a ser executado e, então, faz o chaveamento. Quando o
processador está em modo usuário, ele só aceita chamadas para leitura e escrita de endereços
de memória que estejam dentro dos limites impostos pelos registradores-limite. Caso isso não
seja respeitado, uma interrupção será gerada e o sistema operacional será novamente
carregado para informar o acesso ilegal da memória.
Quando o processador volta para o modo supervisor, esse mecanismo é desligado, pois
o sistema operacional deve possuir privilégio de acesso total à memória, inclusive de
endereços pertencentes a processos de usuário. Isso é necessário pois muitas vezes o sistema
operacional precisa escrever a resposta de uma interrupção gerada por algum processo de
usuário em uma região de memória pertencente a ele, para que o mesmo consiga acessá-la.
A Figura 9 demonstra como a memória principal é organizada, delimitando áreas
específicas para o sistema operacional e processos de usuário.
Figura 9. Ilustração da organização da memória principal
Fonte: Adaptado de OLIVEIRA; TOSCANI; CARISSIMI (2010).
2.2.1.6
Escalonamento de processo
A tarefa de escolher quando o processador deve trocar de processo em execução é do
escalonador, também chamado de agendador de processos. Cabe a ele também decidir qual
34
processo no estado “apto” deverá possuir o processador na sequência. Existem vários
algoritmos diferentes para implementá-lo, mas este deve ser escolhido com atenção
(OLIVEIRA; TOSCANI; CARISSIMI, 2010).
Existem dois pontos-chave que um escalonador procura, embora um acabe
prejudicando o outro, são eles: (i) throughput (vazão) e (ii) turnaround (tempo de resposta ao
usuário) (OLIVEIRA; TOSCANI; CARISSIMI, 2010).
Um alto índice de throughput significa que o processador está altamente otimizado,
quase sempre ocupado com processos “aptos”, dessa forma produz-se mais em menos tempo.
Ao colocar um processo em execução, não se tem a necessidade de trocá-lo antes que o
mesmo termine, pois o próprio procedimento de troca de processos no processador tem um
custo.
Contudo, isso fere gravemente o índice de turnaround, pois normalmente os processos
de interface gráfica do usuário é executado em processos diferentes dos programas que ele
executa suas tarefas, ou ainda que só permita a execução de uma shell por vez, fazendo que
outros usuários tenham que esperar mais tempo para poderem executar outra tarefa.
Para um bom índice de turnaround é necessário que as trocas de processos em
execução aconteçam com mais frequência. Assim o usuário tem uma impressão sólida de
simultaneidade e, também, é possível que vários usuários consigam usar o computador com a
mesma impressão. Na prática, o escalonador tem que agir mais rapidamente e compartilhar o
processador de forma justa entre os processos, mantendo sempre um bom andamento de cada
um deles.
Contudo, não se deve ter trocas excessivas pois isso deixaria o índice de throughput
muito baixo, a ponto de também causar insatisfação ao usuário por demorar mais a executar
sua tarefa principal.
Para Tanenbaum, um bom algoritmo de escalonamento de processos deve ser um
equilíbrio entre as seguintes características: (i) imparcialidade, dando a cada processo um
tempo justo no processador, (ii) a eficiência, mantendo o processador ocupado 100% do
tempo se possível, (iii) throughput maximizado e (iv) turnaround minimizado
(TANENBAUM, 2010).
35
Em algoritmos mais complexos, o escalonador separa os processos em grupos iobound (alto uso de entrada e saída) e cpu-bound (alto uso do processador). Isso possibilita
uma melhor distribuição de recursos do sistema, já que enquanto um processo espera por
dados de entrada e saída de outro dispositivo, outro processo pode ocupar o processador.
Quando a entrada e saída é executada, o processo dependente volta a ganhar prioridade pois
provavelmente irá ocupar o processador por pouco tempo e então solicitará mais entrada e
saída. Assim cada recurso do computador mantem-se ocupado por mais tempo, fazendo com
que algumas coisas (não o processamento) sejam executadas paralelamente (OLIVEIRA;
TOSCANI; CARISSIMI, 2010).
Dentre os principais algoritmos de escalonamento, estão: (i) First-in first-out (ordem
de chegada), (ii) Shortest job first (processo menor antes), (iii) por prioridade, (iv) múltiplasfilas e (v) Round Robin (por fatia de tempo).
FIFO (First-in first-out, primeiro a entrar é o primeiro a sair)
Este é o algoritmo mais simplista, mas o menos eficiente na maioria dos casos. Ele
consiste em fazer uma fila única de processos ao processador, que serão executados em ordem
de chegada.
Neste modelo, um processo somente sai do processador quando efetua uma chamada
de sistema ou quando termina. Por este motivo o índice de turnaround fica muito prejudicado,
perdendo sensação de paralelismo do computador. Outro problema é que ao executar um
processo característico de cpu-bound, todos os outros processos esperarão muito até
conseguirem a vez no processador (OLIVEIRA; TOSCANI; CARISSIMI, 2010).
A Figura 10 mostra como seriam executados os processos neste algoritmo, sendo o
eixo x o tempo de processamento e o eixo y os processos.
36
Figura 10. Ilustração de execução de processos usando FIFO
Fonte: Adaptado de OLIVEIRA; TOSCANI; CARISSIMI (2010).
SJF (Shortest job first, trabalho mais curto primeiro)
Este algoritmo consiste basicamente em ordenar a fila de processos baseado no tempo
que cada processo irá utilizar. Com isso, a média de espera por processo melhoraria, mas
também poderia cair em situações piores. Se a todo momento processos menores entrassem na
fila, os processos maiores nunca seriam executados, entrando em um estado de postergação
indefinida (OLIVEIRA; TOSCANI; CARISSIMI, 2010).
O grande fato que impossibilita que este algoritmo seja implementado é que não se
consegue prever o futuro, ou seja, prever com precisão quanto um processo vai consumir de
tempo ininterrupto de processador. Mesmo assim, ele é muito útil para comparar resultados
em simulações com outros tipos de algoritmos, por fornecer o limite do tempo mínimo de
espera de processos.
A Figura 11 ilustra como seriam executados os processos com esse conceito.
37
Figura 11. Ilustração de execução de processos usando SJF
Fonte: Adaptado de OLIVEIRA; TOSCANI; CARISSIMI (2010).
Por prioridade
O escalonamento por prioridade funciona de forma parecida com o SJF, ordenando-se
uma lista de processos, mas desta vez por sua prioridade. A prioridade não precisa ser dada
necessariamente dentro do escalonador, é comum até que esta seja designada externamente
conforme interesses dos usuários (OLIVEIRA; TOSCANI; CARISSIMI, 2010).
Um exemplo de prioridade seria priorizar mais os processos de professores e menos os
processos de alunos. Também poderia ser por processos que interferem na interface do
usuário, tornando processos do sistema operacional menos prioritários.
Caso a prioridade seja definida pela característica io-bound do processo, este se
assemelharia muito com o SJF, pois estes seriam executados rapidamente e entrariam na fila
pelo dispositivo de entrada e saída, dando a vez para os demais processos (OLIVEIRA;
TOSCANI; CARISSIMI, 2010).
Um cuidado à parte do escalonador que implementa este modelo, é que se processos
com prioridades altas são frequentemente criados, os processos de baixa prioridade correm o
risco de também sofrerem a postergação indefinida (OLIVEIRA; TOSCANI; CARISSIMI,
2010).
Para que isso não ocorra, é necessário que se implemente também um algoritmo de
aging (envelhecimento). Conforme o tempo que os processos ficam na fila, estes vão
38
envelhecendo e, quando chegam a um determinado limite podem ganhar prioridades mais
altas temporariamente. Dessa forma garante-se que os processos menos prioritários sejam
executados eventualmente, mesmo que demore um pouco. Ao ganharem a vez de serem
processados, caso saiam do processador por alguma chamada de sistema e tenham que entrar
na fila de processos novamente, estes entram de novo com sua prioridade baixa (OLIVEIRA;
TOSCANI; CARISSIMI, 2010).
Outra otimização geralmente feita é a troca de processos do processador sem nenhuma
chamada de sistema ou interrupção, simplesmente pelo fato de que um processo com
prioridade mais alta entrou na fila, então deve-se parar a execução do processo menos
prioritário que está em execução para que ceda sua vez. Isso é o que faz valer todo o esforço
de se priorizar todos os processos. Essa característica é o que leva o algoritmo a ser chamado
de preemptivo (OLIVEIRA; TOSCANI; CARISSIMI, 2010).
Múltiplas filas
Uma evolução dos algoritmos FIFO e por prioridade é tratar tipos de processos em
diferentes filas. Exemplo: processos de foreground (de primeiro plano para o usuário) e de
background (segundo plano). Pode-se dizer que a fila de foreground recebe 60% do tempo de
processador, enquanto a fila background recebe 40%, para que o usuário tenha mais sensação
de continuidade dos processos de interface, fazendo com que não aconteça travamentos. Outra
forma é dizer que processos de background só executam quando a fila de foreground está
vazia (OLIVEIRA; TOSCANI; CARISSIMI, 2010).
Os algoritmos anteriores podem ser aplicados para cuidar da política de escalonamento
dentro de uma mesma fila. Exemplo: na fila foreground os processos são ordenados por
prioridade, enquanto que na fila background os processos são executados em fila FIFO. Outra
forma é dividir o processador em fatias de tempo, que será abordada a seguir.
Round robin
Também conhecido por escalonamento por fatia de tempo. Neste método define-se um
tempo máximo para que um processo fique no processador, chamado de quantum, e organizase os processos em uma fila FIFO. Quando esse tempo termina, o escalonador tira o processo
do processador, o coloca no final da fila, e seleciona o próximo da fila para ser executado.
Quando um processo para de ser executado por uma chamada de sistema, o escalonador
39
apenas seleciona o próximo processo apto da fila (OLIVEIRA; TOSCANI; CARISSIMI,
2010).
Uma característica desse algoritmo é ser bastante justo pois onde todos os processos
ganham a mesma chance de processamento. Com isso, não há a possibilidade de postergação
indefinida.
A fatia de tempo de execução é controlada por um relógio de tempo real
implementado em hardware. Quando o tempo de execução termina, esse relógio causa uma
interrupção de alta prioridade e força o processador a carregar o escalonador de processos já
em modo supervisor (OLIVEIRA; TOSCANI; CARISSIMI, 2010).
A grande atenção nesse algoritmo é o tamanho do quantum. Caso esse seja muito
grande, a ponto de todos os processos conseguirem executar por completo na primeira rodada,
produz o mesmo resultado que uma FIFO. Caso este seja muito pequeno, igual ou menor do
que o tempo de chaveamento de contexto, sua eficiência cairia muito, tornando o processador
ocupado em mais da metade do tempo com a troca de processos (OLIVEIRA; TOSCANI;
CARISSIMI, 2010).
Quando se procura mais eficiência, aumenta-se o quantum, e quando se precisa de
mais paralelismo, diminui-se o quantum, cuidando-se com os limites citados acima.
A Figura 12 ilustra como os processos seriam executados no processador, ignorandose o tempo das trocas de processos.
Figura 12. Ilustração de execução de processos usando Round Robin
40
Fonte: Adaptado de OLIVEIRA; TOSCANI; CARISSIMI (2010).
Por ser um equilíbrio entre eficiência e simplicidade, este algoritmo foi escolhido para
implementar o escalonador de processos do PIqueno.
2.3 CONSTRUTIVISMO
Para se falar de construtivismo tem-se que ter em mente a palavra “ideia”. Tanto nos
aspectos sociais como nos aspectos cognitivos, o que sustenta o indivíduo é a ideia formada a
partir da interação dos aspectos sociais e cognitivos: o homem não é um mero produto do
ambiente.
O homem é formado pelas relações de interação entre esses dois fatores – social e
cognitivo.
Assim, “o conhecimento não é uma cópia da realidade, mas, sim, uma construção do
ser humano.” (CARRETERO, 1997, p. 10); essa construção a que se refere o autor nada mais
é do que a representação que temos da nova informação interagindo com a atividade que
realizaremos sobre essa nova informação.
Precedida pelas teorias empirismo de Skinner – em que encontramos a Psicologia
behaviorista (ou do comportamento) – e da psicologia Gestalt (ou da forma), a teoria
construtivismo se apresenta com base na construção e na interação: o conhecimento não se
centraliza no sujeito nem no objeto/informação, mas, sobre tudo, na construção e na interação
do sujeito com o objeto.
Na psicologia behaviorista o conhecimento é a relação do objeto com o sujeito, tendo
o objeto supremacia sobre o sujeito; já na Psicologia Gestalt o conhecimento é o resultado da
interação do sujeito com o objeto, tendo supremacia do sujeito sobre o objeto.
Para Piaget o conhecimento é a interação do sujeito com o objeto sem, contudo, que
haja supremacia de um sobre o outro (FRANCO, 1995).
O objeto e o sujeito não são a condição fundamental para a existência do
conhecimento, mas no “concurso da ação que se instalará a diferenciação entre o sujeito que
conhece e o objeto a ser conhecido.” (FRANCO, 1995).
41
Assim, Piaget considera que o conhecimento surge da ação, ou seja, de uma ação que
o sujeito exerce sobre o objeto. Esta ação não se configura como necessariamente prática, mas
também mental.
Para Vargas (2008), a interiorização das informações se processa de maneira diferente
para cada indivíduo; dependendo da experiência ou situação em que se deu esse processo, ele
será arquivado.
Assim, “cada ser humano vai tecendo sua rede de informações” (Vargas 2008, p.32)
que formará o seu conhecimento prévio.
Segundo Smith (1989),
O que possuímos em nossas cabeças é uma teoria de como é o
mundo, uma teoria que é a base de todas as nossas percepções e
compreensão do mundo, a raiz de todo aprendizado, a fonte de
esperanças e medo, motivos e expectativas, raciocínio e criatividade.
E esta teoria é tudo que temos. Se podemos extrair sentido do
mundo, isto ocorre devido à interpretação de nossas interações com
o mundo, à luz de nossa teoria (SMITH, 1989, p. 22).
A teoria a que se refere Smith nada mais é do que o conjunto de tudo que já
aprendemos, de todos os conceitos, informações do aspecto cognitivo e motor, que se
encontram armazenadas numa estrutura mental a que os estudiosos da mente chamam de
memória de longa duração. A interpretação de interações, como coloca Smith, reforça a ideia
postulada pelo construtivismo de que o conhecimento de um indivíduo não representa uma
realidade apenas copiada, mas sobre tudo interpretada, construída por este indivíduo. Esta
interpretação a que nos referimos, referenciando Smith, é resultado da interação do indivíduo
com o momento da aprendizagem, levando-se em conta o contexto situacional desse
momento.
Desta forma, “a memória é a reprodução mental das experiências captadas pelo corpo
por meio de movimentos e dos sentidos. Essas representações são evocadas na hora de
executar atividades, tomar decisões e resolver problemas, na escola e na vida” (LIMA, 2005,
p.52)
42
A interpretação ocorrida no momento da aprendizagem acontece diariamente, sempre
que estejamos em contato com o novo conhecimento, seja realmente aprendendo ou apenas
reformulando conceitos aprendidos anteriormente.
Assim, na teoria construtivista o indivíduo “aprende partindo de suas próprias ações
em relação ao mundo que o cerca, ou seja, ele é capaz de interagir sobre as informações que
está interiorizando” (VARGAS, 2008, p.54).
Essa interação nada mais é do que o indivíduo refletindo sobre sua aprendizagem no
processo de aprendizagem. A reflexão da aprendizagem se dá no momento em que, ao iniciar
o processo de aprendizagem, o conhecimento prévio do indivíduo entra em ação juntamente
com o conhecimento novo. Nessa relação entre conhecimento prévio e conhecimento novo
“Não podemos esquecer que o elo significativo é o princípio fundamental da reestruturação da
rede de conhecimentos” (VARGAS, 2008, p. 54, grifo nosso), ou seja, a aprendizagem.
Entende-se como elo significativo toda ligação de uma informação com outra através de uma
relação semântica, significativa, relacionada.
Aqui convém falar-se das redes de aprendizagem. Na representação mental de tudo
que se aprende, formam-se redes; essas redes representam todas as informações que já
armazenamos e cada nó que liga uma ponta da rede à outra é o elo significativo que os
relacionam e que pode ser diferente para cada indivíduo, pois depende do contexto situacional
da aprendizagem e também de toda a estrutura cognitiva de cada um.
Assim, se o processo de aprendizagem ocorre porque temos uma estrutura cognitiva
ou conhecimento prévio aliado a uma capacidade de percepção da realidade, do mundo
exterior; assim, podemos dizer que a aprendizagem depende mais do próprio sujeito (do que
lhe é interior) do que do que lhe é apresentado na situação de aprendizagem.
Piaget explica, então, a aprendizagem através da interação do conhecimento que está
no sujeito (de que trata a teoria Gestalt) com o objeto de aprendizagem (de que trata a teoria
Behaviorista).
Assim, com sua estrutura cognitiva o indivíduo segue aprendendo partindo de suas
próprias ações em relação às situações que o cercam, ou seja, com suas informações já
armazenadas é capaz de interagir com as novas, reformulando suas redes de conhecimento.
43
Piaget conclui que o conhecimento se dá através dessa ação; ele não só acontece da
ação, como sempre será uma ação, mesmo que esta ação seja somente a nível mental. Assim o
conhecimento por mais teórico que seja resulta de uma ação do indivíduo sobre o objeto de
aprendizagem.
Desta forma, através desta interação o indivíduo vai construindo seu conhecimento,
sua estrutura cognitiva - daí a teoria de Piaget chamar-se “Construtivismo”.
Para Franco,
(...) se encararmos o construtivismo como uma “teoria”, ele se
esvaziará e não trará novidades. Mas se ele se tornar um instrumento
para ajudar o professor a entender a realidade do seu aluno, e a partir
deste entendimento ele passar a criar modos (métodos e técnicas) de
agir em sala de aula, então sim estaremos resgatando a “Teoria”. E
mais ainda, devolvendo à Pedagogia o caráter científico que muitas
vezes foi negligenciado nesta área do conhecimento (FRANCO,
1995, p. 15).
Essa teoria inspirou o desenvolvimento deste projeto. O PIqueno possibilitará uma
interação em que, tanto o erro como o acerto fazem parte de todo o processo de aprendizado.
Para Piaget o erro é algo tão positivo tanto quanto o acerto, pois ele acredita que ambos levam
o indivíduo a elaboração de hipóteses a respeito da realidade que está vivendo naquele
momento e, assim, usando seus próprios procedimentos para experimentá-las ou testá-las.
Entende-se também que no indivíduo em situação de aprendizagem,
(...) o erro (...) pode ser resultado de sua própria atividade
assimilativa, da aplicação dos seus esquemas mentais (ou de ação) a
determinado objeto ou conteúdo. Quando a atividade assimilativa
resulta em erro, e principalmente se de forma repetida, ocorre uma
desequilibração das estruturas cognitivas (...). Isso faz com que ela,
por meio da atividade cognitiva, modifique seus esquemas, o que
resulta em uma reequilibração e, portanto, no aperfeiçoamento de
sua maneira de agir e de pensar e em um nível mais complexo de
conhecimento sobre o objeto (FONTANA, 1996, p. 71).
44
É nesta interação de erros e acertos, no processo em que o aluno usará tanto seu
conhecimento prévio quanto a situação de aprendizagem a ele apresentada, que o PIqueno terá
seu espaço, otimizando o processo educacional de sistemas operacionais.
Entende-se que como se vai ensinar e o que se pretende ensinar dependerá das
habilidades e conhecimento com que serão construídas as atividades programadas. Para
Bruner (1975, p.79) “a arte de programar a máquina é, naturalmente, uma extensão da arte de
ensinar”. A utilização dos recursos e dispositivos utilizados harmonicamente num sistema é o
maior desafio do professor na organização do processo do ensino e aprendizagem, em que
atuará somente como mediador.
Isso quer dizer que o professor oferecerá o apoio, conduzindo o processo através da sua
instrução, ou seja, que ele considere o conhecimento prévio do aluno, incentivando-o e
proporcionando atividades que o levem a novas descobertas. Essa relação de instrução e
apoio, ou mediação de Piaget, é colocada por Bruner (1986: 74) como Instructional
Scaffolding.
45
3 DESENVOLVIMENTO
As características do PIqueno, como linguagem de programação e plataforma, foram
escolhidas com base em uma análise de similares. Com base nos resultados obtidos e exibidos
na Tabela 1, observou-se uma lacuna na arquitetura ARM e com baixo grau de complexidade.
É nesta que o PIqueno foi projetado.
Nome
MINIX
MikeOS
KolibriOS
MenuetOS
Pintos
FreeNOS
PIqueno
Plataforma
x86/ARM
x86
x86
x86
x86
x86
ARM
Linguagem
Assembly/C
Assembly
Assembly
Assembly
Assembly/C
Assembly/C++
Assembly/C
Linhas de código
6.000
9.606
128.000
36.000
30.000
2.000
1.500
Escalonador
Multi Priority Round Robin
Multi Priority Round Robin
Round Robin
Round Robin
Round Robin
Propósito
Acadêmico
Open Source
Open Source
Acadêmico
Experimental
Acadêmico
Tabela 1. Tabela comparativa de sistemas operacionais semelhantes e o PIqueno
A arquitetura ARM foi escolhida por ser razoavelmente mais simples e barata do que
x86. Fazê-lo em arquitetura x86 incentivaria indiretamente os alunos a testarem suas
modificações em computadores de laboratório, o que não era a intenção deste projeto.
O Raspberry Pi foi escolhido por ter propósitos muito semelhantes ao deste projeto, a
aprendizagem e ensino. Além de possuir uma arquitetura ARM, este é relativamente barato
em relação a outros computadores, custando 35 Dólares no Reino Unido e 150 Reais no Brasil
(em meados de maio de 2013).
O projeto original previa somente a utilização da linguagem Assembly, porém,
detectou-se que a toolchain gcc-arm-linux-gnueabihf poderia ser útil ao compilar códigofonte em C para um arquivo binário do ARM, através de uma plataforma x86. Isso permitiu
com que a maior parte do código-fonte do PIqueno fosse feito em C, com o objetivo de tornalo mais claro para entendimento.
Para potencializar o sucesso de aprendizagem, seu código fonte foi feito para ser mais
legível do que otimizado, tendo as partes que envolvem o escalonador de processos bem
comentadas.
O código-fonte foi desenvolvido e testado em ambiente Linux (Ubuntu 13.04),
utilizando o editor Geany, que possui realce de sintaxe para linguagens de programação e
pode ser encontrado no repositório oficial do Ubuntu. O mesmo pode ser editado em qualquer
editor ou IDE.
46
A compilação do código-fonte foi feito através do comando make, que lê as
configurações dos arquivos Makefile e linkscript e faz a compilação utilizando a toolchain
personalizada para ARM. O código-fonte compilado tem como resultado o arquivo
kernel.img.
Para depuração foi utilizado um fork do qemu (software de virtualização), com
configurações
especiais
para
o
Raspberry
Pi,
disponível
em
https://github.com/Torlus/qemu/tree/rpi, em conjunto com a ferramenta gdb, disponível no
repositório do Ubuntu. O qemu pode ser carregado com o arquivo kernel.img, e assim
executado virtualmente. Uma vez em execução, pode-se utilizar a ferramenta gdb para
configurar breakpoints e controlar a execução do PIqueno no qemu. Isso torna o processo de
programação mais ágil.
Para testes reais, o arquivo kernel.img foi colocado na partição de boot do cartão de
memória e executado no próprio Raspberry Pi, ligado a uma televisão.
O código-fonte do PIqueno foi disponibilizado no GitHub, onde pode ser obtido
através da URL https://github.com/danielbathke/PIqueno.
Com o código-fonte completo, o professor poderá utilizar a estratégia pedagógica
Instructional Scaffolding, mostrando primeiro o PIqueno completo, funcional e navegando
sobre a estrutura dos arquivos de código-fonte. Posteriormente, o mesmo pode propor uma
atividade para reimplementar um pedaço simples à sua escolha, utilizando outro algoritmo.
Um bom exemplo seria reimplementar o escalonador de processos utilizando outro algoritmo
além do Round Robin. Após essa primeira atividade, uma outra semelhante pode ser proposta,
mas agora em uma parte de maior complexidade. Esse ciclo poderá se repetir conforme o
objetivo do professor.
Para alcançar o objetivo de ensino no conceito de escalonador de processos, foi
desenvolvido os procedimentos de inicialização, uma interface gráfica simples, a organização
básica de um processo e um escalonador de processos. O algoritmo escolhido foi o Round
Robin, por ser eficiente e ao mesmo tempo simples.
Pensando na facilidade de comparação de resultados feita pelos alunos, depois de
modificarem o PIqueno, identificou-se a necessidade de prover algumas informações em
47
tempo de execução. Então foram programadas saídas na interface gráfica, exibindo
informações a cada troca de processo em execução.
3.1 DIAGRAMA DE CASOS DE USO
A Figura 13 ilustra o diagrama de casos de uso do Aluno quanto ao PIqueno.
Figura 13. Diagrama de Casos de Uso
Conforme ilustrado na Figura 13, há cinco casos de uso previstos para o PIqueno. O
primeiro é aquele onde o Aluno realiza o download do código-fonte do sistema operacional. O
segundo prevê que o Aluno fará a modificação do código-fonte do sistema utilizando uma
IDE, visando alcançar objetivos propostos em atividades na disciplina.
Em seguida, o terceiro caso de uso é o momento em que o Aluno recompila o códigofonte, através da mesma IDE, para gerar os arquivos executáveis e poder testar suas
48
modificações. Após recompilar, os executáveis são copiados para o cartão de memória, que
será inserido no Raspberry Pi. Após inserir o cartão de memória, o Aluno poderá ligar o
minicomputador para dar início ao processo de boot no PIqueno, caracterizado como o quarto
caso de uso.
Por último, com o PIqueno em execução, o aluno verá seus processos exemplo sendo
executados, visualizando cada mudança de processo do processador até que terminem-se
todos. Essa visualização será a parte essencial para que o aluno veja na prática as
consequências de suas mudanças no código.
3.2 REQUISITOS
Este trabalho possui 5 requisitos não funcionais e 10 requisitos funcionais.
3.2.1 Requisitos não funcionais
1. O PIqueno deverá ser implementado em Assembly e C;
2. O PIqueno deverá ser implementado na plataforma Raspberry Pi;
3. O PIqueno deverá ter seu código-fonte aberto;
4. O código-fonte deverá ser publicado no GitHub;
5. O código-fonte deverá ter todas as suas partes essenciais comentadas.
3.2.2 Requisitos funcionais
1. O usuário deverá conseguir fazer o boot no Raspberry Pi com um cartão de memória;
2. O usuário deverá ter um feedback visual durante processo de boot;
3. O PIqueno deverá organizar os processos em uma árvore;
4. O PIqueno deverá possuir um escalonador de processos;
5. O escalonador de processos deverá utilizar o algoritmo Round Robin;
6. O usuário deverá visualizar a execução dos processos na tela;
7. O PIqueno deverá permitir a execução de processos simultâneos;
8. O PIqueno deverá possuir 3 aplicativos de exemplo para testes do usuário.
3.3 DIAGRAMA DE DOMÍNIO
49
A Figura 14 ilustra o Diagrama de Domínio do PIqueno.
Figura 14. Diagrama de Domínio do PIqueno
O PIqueno seguirá os conceitos consolidados de sistemas operacionais atuais, tendo
em sua organização partes como Scheduler, Memory, Process, Framebuffer, Interrupts e
Syscall. O código fonte dessas partes, bem como seus nomes, serão mantidos em Inglês para
incentivar o uso do Inglês instrumental do aluno.
A parte inicial do PIqueno, chamada de start, é desenvolvida em Assembly, contendo
partes essenciais de inicialização como inicializar as stacks de cada modo do processador,
ligar o acesso à memória e invocar o procedimento initsys, como visto no Quadro 1.
# start.s
.global
_start:
_start
50
mov r4, #0x80000000
// Configuring stack to every cpu mode that we will use
cps #0x13
/* Change to supervisor (SVC) mode */
add sp, r4, #0x2400
cps #0x17
/* Change to Abort mode */
add sp, r4, #0x2800
cps #0x12
/* Change to IRQ mode */
add sp, r4, #0x2c00
cps #0x1f
/* Change to system mode */
add sp, r4, #0x3c00
/* Turn on unaligned
mrc p15, #0, r4, c1,
orr r4, #0x400000 /*
mcr p15, #0, r4, c1,
memory access */
c0, #0
1<22 */
c0, #0
/* Jump to memory map initialisation code */
b initsys
Quadro 1. Código de inicialização
A base dos endereços das stacks é o endereço 0x80000000, configurada no registrador
r4. À partir daí, configura-se cada stack alterando o modo do processador com o comando cps
e somando o endereço base (r4) com o endereço da stack. O comando b faz com que o
processador salte para o procedimento initsys, feito em C, que termina de configurar um
gerenciamento básico da memória, se caracterizando como boot. Após isso, invoca-se o
procedimento Main.
O procedimento Main, já em um nível de abstração maior, inicializa o vetor de
interrupções (Interrupts), o Framebuffer (espaço da memória reservado para o dispositivo de
vídeo) e a Memory (Memória). Após essas inicializações, são registrados na árvore de
processos o processo pai, referente ao procedimento Main, e os processos filhos que são os
processos exemplo. Em seguida, com toda organização feita, o timer do processador é
inicializado e as interrupções ligadas, fazendo com que os processos comecem a ser
alternados no processador. O procedimento está demonstrado no Quadro 2.
# main.c
...
void main(unsigned int r0, unsigned int machtype, unsigned int atagsaddr)
{
// Initialise basic memory management
51
mem_init();
// Initialize led management
led_init();
// Initialize framebuffer (video reserved memory location)
fb_init();
// First boot message
console_write("Welcome to PIqueno\n\n");
// Creating my process in the process table
create_main_process();
// Create three sample processes with name and address of function
fork("Sample process 1", &sample_process_1);
fork("Sample process 2", &sample_process_2);
fork("Sample process 3", &sample_process_2);
// Configuring iterrupts
interrupts_init();
// Do nothing and wait for scheduler to stop executing me
main_endloop();
}
...
Quadro 2. Procedimento Main
A função fb_init inicializa o espaço da memória reservado para o framebuffer, que é
lido pelo GPU. Este é responsável por gerar o sinal de vídeo através da porta HDMI. Depois
disso, pode-se gerar mensagens na tela através da função console_write. O procedimento
create_main_process é responsável por gerar um processo para o programa que se está
executando no momento, o Main, conforme visto no Quadro 3.
# scheduler.c
...
// Function to create the main process
void create_main_process() {
// Creating process
process main_process;
main_process.id = process_count;
main_process.name = "Main";
main_process.pc = &main_endloop;
main_process.times_loaded = 1;
main_process.status = PROCESS_STATUS_ZOMBIE;
52
// Gets the actual stack address
unsigned int stack_pointer;
asm volatile ("MOV %0, SP\n\t" : "=r" (stack_pointer) );
// Use it as base address processes stack pointer
stack_base = stack_pointer;
// Output the stack address
console_write("Main stack is 0x");
console_write(tohex(stack_pointer, 4));
console_write("\n");
// Saving the process in the process table
process_list[process_count] = main_process;
// Set it as the current active process
active_process_index = process_count;
// Increments the process counter
process_count++;
}
...
Quadro 3. Procedimento create_main_process
O procedimento cria um novo process (Quadro 4) e o salva no array de processos.
Nota-se que este processo é criado com o status PROCESS_STATUS_ZOMBIE, que indica
que este processo não deve mais ser colocado em execução, mas que não pode terminar
completamente pois ainda terá processos filhos aptos a executarem. Isso serve para que os
processos seguintes possam ser gerados e associados com um processo pai.
# process.h
...
typedef struct {
unsigned int
char *
unsigned int
unsigned long
unsigned long
unsigned int
unsigned int
id;
name;
parent;
stack_pointer;
pc;
times_loaded;
status;
} process;
...
Quadro 4. Definição da struct process
53
A função fork cria novos processos filhos do processo que a chama. Esta tem dois
parâmetros, o nome do processo e o endereço de memória em que o seu código está. Esta
funciona semelhante a função create_main_process, com algumas diferenças. A principal
delas é o status do novo processo, que recebe o valor PROCESS_STATUS_WAITING,
informando ao escalonador que este processo está apto para ser executado, esperando sua vez
para que o processador o execute.
Quando os processos terminam de executar, eles invocam uma Syscall, através de uma
software interruption (interrupção de software) com o comando SWI (Quadro 5), para que
sejam marcados com estado de termino. Nesse estado, o processo é ignorado pelo escalonador
de processos.
# process.c (com processador no modo user)
...
// Just some counting for easy debug on the screen. Simulate user process.
void sample_process_2() {
console_write("Starting process 2 ");
int to = 300;
int i, j;
for (i=0; i<to; i++) {
console_write(todec(i,0));
console_write(" ");
for (j=0; j<to*to; j++) {
asm volatile("NOP");
}
}
// Call software interrupt #0 (terminate)
asm volatile("SWI #0");
}
...
# syscall.c (já com processador em modo system)
...
switch (swi) {
case SYSCALL_TERMINATE_PROCESS:
console_write("Invoking syscall
terminate_process()");
console_write("\n");
54
// Calling the right intra-kernel procedure
terminate_process();
break;
}
...
Quadro 5. Processo invocando uma Syscall para término
Analisando novamente o código do procedimento Main, encontra-se a função
interrupts_init, que configura o vetor de interrupções do processador (Quadro 6). Esta contém
endereços de procedimentos para cada tipo de interrupção, dentre elas a interrupção por IRQ e
a interrupção por software, vista previamente.
# interrupts.c
...
void interrupts_init(void)
{
// Set interrupt base register
asm volatile("mcr p15, 0, %[addr], c12, c0, 0" : : [addr] "r"
(&interrupt_vectors));
// Turn on interrupts
asm volatile("cpsie i");
// Enable ARM timer IRQ
*irqEnableBasic = 0x00000001;
// Interrupt every x * 256 (prescaler) timer ticks
*armTimerLoad = 0x00000400;
// Timer enabled
*armTimerControl = 0x000000aa;
}
...
__attribute__ ((naked, aligned(32))) static void interrupt_vectors(void)
{
asm volatile("b bad_exception\n" // Reset
"b bad_exception\n"
// Undefined
"b interrupt_swi\n"
// Software interrupt
"b interrupt_prefetch_abort \n"
// Prefech abort interrupt
"b interrupt_data_abort \n"
// Data abort interrupt
"b bad_exception;\n"
// Unused vector
"b interrupt_irq \n"
// IRQ interrupt
"b bad_exception\n"
// Fast Interrupt (FIQ),
not used
);
55
}
...
Quadro 6. Função interrupts_init configurando vetor de interrupções
Após a criação do processo principal (create_main_process), da criação dos processos
do usuário (fork) e do procedimento de inicialização de interrupções (interrupts_init), o
sistema está pronto para começar a executar os processos do usuário. Nesse momento, o
procedimento Main termina invocando a função main_endloop, que apenas aguardará a
interrupção do timer para ser tirado de execução.
Quando o timer expira, é gerada uma interrupção de IRQ. Em sistemas operacionais
reais, as interrupções de IRQ podem ser geradas por vários dispositivos, como teclado e
mouse por exemplo. No caso do PIqueno, o timer é o único dispositivo que gera uma
interrupção de IRQ, então, dados os tratamentos iniciais para salvar os registradores na stack
do processo atual, invoca-se o escalonador através da função schedule_timeout, visto no
Quadro 7.
# interrupts.c
...
// IRQ
__attribute__ ((interrupt ("IRQ"))) void interrupt_irq(void)
{
// This function starts on IRQ mode
// Push all registers into the IRQ mode stack (R13)
asm volatile("push {R0-R12}");
// Put LR register of IRQ mode (PC of interrupted process) on R0
asm volatile("MOV R0, LR");
// Change to system mode
asm volatile("cps #0x1f");
// Push R0 (interrupted PC) to the system mode stack
asm volatile("push {R0}");
// Return to IRQ mode
asm volatile("cps #0x12");
// Pop all registers again
asm volatile("pop {R0-R12}");
56
// Return to system mode
asm volatile("cps #0x1f");
// Push all registers into the system mode stack
asm volatile("push {R0-R12}");
// Push the interrupted LR to system mode stack
asm volatile("push {LR}");
// Copy the processor status to R0
asm volatile("MRS R0, SPSR");
// Push the processor status to system mode stack
asm volatile("push {R0}");
// Return to IRQ mode
asm volatile("cps #0x12");
// Copy LR to R0
asm volatile("MOV R0, LR");
// Back to system mode
asm volatile("cps #0x1f");
unsigned long pc;
unsigned long stack_pointer;
// Getting pc and stack just to debug
asm volatile ("MOV %0, R0\n\t" : "=r" (pc) );
asm volatile ("MOV %0, SP\n\t" : "=r" (stack_pointer) );
// Invert led to inform context switch activity
led_invert();
// Jump to scheduler to do the context switch
schedule_timeout(stack_pointer, pc);
}
...
Quadro 7. Tratamento de uma interrupção de IRQ invocando o escalonador
Ao ser invocado, o escalonador salva a stack e o pc do processo atual, mudando seu
status para PROCESS_STATUS_WAITING novamente. Esta é a parte em comum entre a
maioria dos algoritmos de escalonador. Posteriormente, já observando-se as características do
algoritmo Round Robin, seleciona-se o próximo processo da lista apto para execução.
Incrementa-se sua variável times_loaded, para estatísticas, muda-se o status para
PROCESS_STATUS_RUNNING, e por fim recupera-se os valores da stack e do pc com o
comando pop, gerando um branch automático para o novo valor do pc. Este procedimento
está exemplificado no Quadro 8.
57
# interrupts.c
...
void schedule_timeout(unsigned long stack_pointer, unsigned long pc) {
// Saving stack and pc
process_list[active_process_index].stack_pointer = stack_pointer;
process_list[active_process_index].pc = pc;
// Updating process status to waiting
if (process_list[active_process_index].status ==
PROCESS_STATUS_RUNNING) {
process_list[active_process_index].status =
PROCESS_STATUS_WAITING;
}
...
// Get next process id
int next_process = next_waiting_process_index();
// If -1, halt
if (next_process < 0) {
console_write("No more waiting processes, halting.");
halt();
}
// Updating next running process
active_process_index = next_process;
// Increasing statistics, updating status to running
process_list[active_process_index].times_loaded++;
process_list[active_process_index].status = PROCESS_STATUS_RUNNING;
...
// Assembly to load the sp with the new process stack address
asm volatile("MOV SP, %[addr]" : : [addr] "r" ((unsigned long
)(process_list[active_process_index].stack_pointer)) );
// If its is not the first time
if (process_list[active_process_index].times_loaded > 1) {
timer_reset();
// Pops registers
asm volatile("pop
asm volatile("MSR
asm volatile("pop
asm volatile("pop
from the stack
{R0}");
SPSR_cxsf, R0");
{LR}");
{R0 - R12}");
// Turn on interrupt again
asm volatile("cpsie i");
// Pops the last register into PC to resume processing
asm volatile("pop {PC}");
} else {
// Push the first pc address into the stack
58
unsigned long addr = (unsigned long
)(process_list[active_process_index].pc);
asm volatile("MOV R0, %[addr]" : : [addr] "r" (addr) );
asm volatile("push {R0}");
timer_reset();
// Turn on interrupt again
asm volatile("cpsie i");
// Pops the last register into PC to resume processing
asm volatile("pop {PC}");
}
}
...
Quadro 8. Escalonador trocando processos do processador
Se o escalonador não encontrar mais processos aptos para execução, o sistema é
interrompido com o comando Halt do processador.
3.4 CENÁRIO DE UTILIZAÇÃO EM SALA DE AULA
Para uso em sala de aula, criou-se um cenário baseado na teoria de Bruner,
Instructional Scaffolding. O professor dará uma solução parcialmente pronta, e solicitará aos
alunos que a melhorem de acordo com a teoria, mostrando seus resultados na prática.
O PIqueno seria a solução parcial, com o quantum do escalonador configurado para
um grande valor, onde todos os processos do usuário consigam ser executados passando
apenas uma vez pelo processador.
Após demonstrar a solução funcionando, bem como explicar suas saídas na tela, o
professor comentaria sobre a falha em seu estado atual e qual seria o comportamento
esperado. A atividade proposta seria ajustar o escalonador.
Sem mencionar o que esta errado, ou aonde, espera-se que os alunos explorem o
código fonte, conhecendo-o e tendo um contato maior com a programação de um sistema
operacional. O processo de evolução conta com alguns testes, e possivelmente alguns erros.
Estes erros são importantes no processo de aprendizado, onde o aluno constrói seu
conhecimento com base em experiências práticas.
59
A solução adequada para este problema seria ajustar o quantum para um valor
equilibrado. Nem tão pequeno a ponto de retardar muito a execução dos processos, nem tão
alta causando sua execução em 1 ou poucas passagens pelo processador.
Uma próxima atividade, mais complexa que a primeira, seria trocar o algoritmo do
escalonador. Poderia-se propor o desenvolvimento do algoritmo de escalonamento por
prioridade, onde alguns tipos de processo devem receber mais vezes o processador e serem
terminados antes dos processos não-prioritários. Esta atividade requer um nível de
conhecimento mais aprofundado do código-fonte, que a primeira atividade pode proporcionar.
Este é apenas um cenário dos vários possíveis com o PIqueno em seu estado atual.
Essas possibilidades podem aumentar ainda mais com o desenvolvimento contínuo do
PIqueno em outros trabalhos técnicos de conclusão de curso.
60
4 CONSIDERAÇÕES FINAIS
Este trabalho teve por objetivo construir um sistema operacional simples o suficiente
para ser estudado em sala de aula. Trata-se do PIqueno, e com este espera-se diminuir a
lacuna entre a teoria e a prática na disciplina de sistemas operacionais nos cursos de Ciência
da Computação e Engenharia da Computação da Universidade do Vale do Itajaí.
Para que os objetivos estipulados fossem alcansados, foi preciso resgatar os conceitos
de sistemas operacionais previamente estudados, para posteriormente eleger quais conceitos
iniciais seriam abordados nesta primeira versão. O conceito eleito foi o escalonador de
processos. Acredita-se que este conceito, em conjunto com outros dependentes deste, sejam os
mais eficientes para o propósito deste trabalho.
Todas as ferramentas utilizadas neste projeto são livres, o que pode contribuir para
experiências não somente em sala de aula, mas em casa.
Os resultados obtidos não só foram satisfatorios, mas também são prova de que é
possível estimular o desenvolvimento de um sistema operacional em sala de aula, e com isso
elevar a eficiência de ensino e desmistificar os conceitos teóricos da discipina.
Não é objetivo do PIqueno ser um sistema operacional para utilização em situações
reais. Por esse motivo o PIqueno não foi concebido com otimizações de código-fonte, de
desempenho, ou ainda com uma larga escala de hardwares compatíveis.
4.1 TRABALHOS FUTUROS
Um trabalho técnico científico dispõe de pouco tempo para se construir um sistema
operacional completo. Por este motivo, a primeira versão do PIqueno se limita a exemplificar
o conceito de escalonador de processos, e por consequência a estrutura básica de um processo,
chaveamento de contexto, interrupção proveniente do timer e uma chamada de sistema.
Isso deixa uma grande abertura para trabalhos futuros, que podem tanto ser
implementados durante a disciplina ou como trabalhos de conclusão de curso. De fato,
acredita-se que este trabalho inicial trará novos entusiastas para encarar o desafio de continualo.
61
Serão descritas brevemente as possibilidades para trabalhos futuros levantadas neste
ponto do projeto, em uma lista de sugestões de ordem baseada em dependências.
4.1.1 Drivers para dispositivos de E/S
Uma das principais tarefas de um computador é ler e escrever em dispositivos de
entrada e saída, sendo para interagir com o usuário ou para tratar outros dispositivos.
Sugere-se que seja iniciado pelo desenvolvimento de drivers para acesso ao leitor de
cartão de memória, teclado e mouse. Posteriormente, pode-se extender essa funcionalidade
para dispositivos de armazenamento em massa ligados na porta USB.
Isso abrirá a gama de possibilidades de desenvolvimento para o PIqueno. Uma vez
feito isso, o PIqueno começaria a se tornar mais útil para o usuário.
4.1.2 Sistema de arquivos
Para que o sistema operacional possa ler arquivos e executar tarefas do usuário
efetivamente, precisa-se que seja definido e desenvolvido uma camada de compatibilidade
com algum sistema de arquivos já conhecido. Isso irá permitir por exemplo, que as tarefas do
usuário envolvam leitura e escrita em arquivos.
Para manter uma boa compatibilidade com outros sistemas operacionais, recomendase que seja implementado no PIqueno o sistema de arquivos FAT32, reconhecido hoje por
todos os sistemas operacionais modernos. Posteriormente, pode-se desenvolver o
reconhecimento aos sistemas de arquivos NTFS, EXT2 e EXT3, pensando na evolução do
gerenciamento de permissões e segurança.
Esse passo faz-se necessário para que posteriormente possa ser carregado um
programa do usuário à partir de um arquivo.
4.1.3 Gerenciamento de E/S
Trata-se de um aperfeiçoamento do passo anterior. Neste ponto, precisa-se gerenciar o
acesso de entrada e saída através de várias Syscalls. Estas serão invocadas através de
interrupções de software, de maneira que o programador de aplicativo não tenha acesso direto
ao dispositivo de entrada e saída em questão.
62
Talvez ainda não seja possível retirar os aplicativos de usuário de dentro do kernel,
pela falta de definição de arquivo binário. Porém, já seria de grande valia fazer com que as
chamadas de leitura e escrita aconteçam única e exclusivamente através de Syscalls, e sendo
atendidas pelo sistema operacional com processador em modo system.
4.1.4 Definição de arquivo binário
Talvez um dos maiores passos para o PIqueno seja que o usuário consiga carregar e
executar seus aplicativos à partir de arquivos binários. Isso resolve o problema de isolamento
de baixo nível do kernel, fazendo com que os programas sejam carregados de dispositivos de
armazenamento em massa.
Isso permite que o usuário faça um programa em outro computador, o compile, copie
no cartão de memória e execute no PIqueno.
O papel do padrão do arquivo binário é determinar de que forma o programa já
compilado será carregado na memória, para que posteriormente seja executado. O cabeçalho
do arquivo define informações de quais bibliotecas externas serão utilizadas, por exemplo.
Por questões de compatibilidade e utilização de um padrão já consolidado, recomendase que o padrão de arquivo binário seja o ELF, utilizado no Linux.
4.1.5 Compilador para o PIqueno
Com o arquivo binário definido, pode-se criar uma ferramenta externa, para Windows
ou Linux por exemplo, para escrever e compilar um programa em C e transforma-lo já em um
arquivo binário do padrão do PIqueno, contendo informações em seu cabeçalho e o programa
binário para ARM.
Isso facilitaria a criação de ferramentas e outros aplicativos para o PIqueno.
4.1.6 Gerenciamento de memória
O gerenciamento de memória utilizado atualmente não deve suprir todas as
necessidades de um sistema operacional moderno. Com o foco em estudar um sistema
operacional com conceitos modernos, o gerenciamento de memória deve ser atualizado.
63
Deve-se criar um controle de memória por processos, sendo que cada processo deve
receber ou requerer um espaço exclusivo na memória. Os processos devem ser impedidos de
invadir o espaço de outro processo, através de barreiras na memória. O processador ARM
utilizado no Raspberry PI contém uma série de comandos para que esse isolamento seja
possível.
O espaço do kernel do sistema operacional também deverá ser isolado, não sendo
acessado de nenhuma forma por processos do usuário, sendo acessado somente com o
processador em modo system.
4.1.7 Interface de linha de comando
A interface de linha de comando é a ferramenta que faltava para que o usuário possa
utilizar o PIqueno de forma mais interativa, executando tarefas através de comandos
digitados.
Esta é comumente chamada de shell, que expõe os programas a serem executados para
o usuário, e até mesmo chamadas de sistema.
Recomenda-se o estudo em cima do padrão Bash, utilizado em sistemas operacionais
baseados no Unix.
4.1.8 Driver para dispositivo de rede
Trata-se de um conjunto de programas com a função de gerenciar o dispositivo de
rede, possibilitando que o PIqueno possa se logar em uma rede, ganhar um endereço IP, e
interagir com outros computadores.
Sendo um driver, deve ser executado em modo system, não disponível para o usuário.
Este deve entrar em execução logo na inicialização. Recomenda-se a criação de ferramentas
para reiniciar a comunicação da rede cabeada, exibição de informações da rede, como
endereço IP, mascara de rede e gateway.
Primeiramente, pode-se trabalhar com configurações de rede fixas, definidas em
arquivo. Posteriormente pode ser implementado o protocolo DHCP (dynamic host
configuration protocol, protocolo dinâmico de configuração de hospedeiro), que visa obter as
configurações de rede automaticamente, quando houver um servidor DHCP na rede.
64
4.1.9 Conjunto de ferramentas para execução em linha de comando
Constitui-se de aplicativos que extenderão as capacidades do usuário quando
utilizando a interface de linha de comando do PIqueno.
Recomenda-se o estudo em cima dos comandos de sistemas baseados no padrão
POSIX (portable operational system interface based on UNIX, Interface para sistemas
operacionais portáveis baseados em UNIX).
Dentre as tarefas possíveis, estariam a capacidade de (i) listar arquivos, (ii) criar
arquivos, (iii) remover arquivos, (iv) criar diretórios, (v) remover diretórios, (vi) entrar em um
diretório, (vii) sair de um diretório, (viii) listar processos em execução, (ix) exibir o conteúdo
de um arquivo na tela, entre outros.
4.1.10
Gerenciamento de usuários
O gerenciamento de usuários permite um nível maior de isolação de itens de
segurança, como propriedade de arquivos, possíveis permissões, definição de um usuário
administrador do computador.
Também facilita a separação de arquivos, estimulando o uso do computador por mais
de um usuário, simultaneamente ou não.
A definição de um usuário administrador visa a proteção da integridade do sistema
operacional, bem como suas configurações, para que fique o maior tempo possível apto a ser
utilizado por todos os usuários.
4.1.11
Permissões
Em conjunto com o gerenciamento de usuário e um sistema de arquivos mais recente,
pode-se atribuir permissões para cada usuário.
Tanto para arquivos, quanto para dispositivos que podem ser expostos ao usuário
como um arquivo, pode-se aplicar o padrão de permissões POSIX. Nesse padrão, cada
arquivo pode receber 3 octetos (4, 2 ou 1) de permissões, cada um referenciando a um grupo
específico de usuários, bem como seu dono e seu grupo.
65
Dentro de um octeto, quando soma-se 4 refere-se a leitura, 2 refere-se a escrita e 1
refere-se a execução. Sendo assim, 7 significa permissão total, 6 permissão de leitura e
escrita, 5 leitura e execução e 3 escrita e execução.
O primeiro octeto refere-se as permissões do dono do arquivo. O segundo refere-se as
permissões dos membros do mesmo grupo do arquivo. O último octeto descreve as
permissões públicas. Desta forma, 777 significa permissão total para todos usuários, 700,
permissão total somente para o dono, 711, permissão total para o dono e de execução para
membros do mesmo grupo ou usuários públicos.
4.1.12
Interface gráfica
A interface gráfica, já em um nível de maior maturidade do projeto, pode ser feita para
agilizar a execução de tarefas pelos usuários, permitindo sua interação mais precisa com o
mouse, a visualização de gráficos mais elaborados, imagens vídeos, etc.
Cada gerenciador gráfico tem pelo menos um decorador, gerenciador de janelas,
gerenciador de painéis, gerenciador de widgets e um gerenciador de login.
O sucesso do PIqueno não depende deste passo do projeto, mas caso sua evolução
ocorra em grande intensidade, pode-se pensar em implementar uma interface gráfica.
66
REFERÊNCIAS
OLIVEIRA, R. S. de; TOSCANI, S. S.; CARISSIMI, A. da S. Sistemas Operacionais. 4.ed.
Porto Alegre: Bookman, 2010.
MAZIERO, C. Reflexões sobre o ensino prático de Sistemas Operacionais. In: Congresso da
Sociedade Brasileira da Computação, 12., 2002, Florianópolis. Anais do X Workshop Sobre
Educação em Computação da SBC. Florianópolis: Sociedade Brasideira de Computação,
2002. p. 1-12.
TANEMBAUM, A. S. Sistemas Operacionais Modernos. 3.ed. São Paulo: Pearson
Education do Brasil, 2010.
HALFACREE, G.; UPTON, E. Meet the Raspberry Pi. 1.ed. Reino Unido: John Wiley &
Sons, 2012.
SMITH, F. Compreendendo a leitura. Porto Alegre: Artes Médicas, 1989.
LIMA, E. S. É assim que se aprende. Nova Escola, São Paulo, v. 20, n. 179, p. 52-57, jan.
2005.
VARGAS, E. S. M. Psicolinguística. Itajaí: UNIVALI, 2008.
FRANCO, S. R. K. O Construtivismo e a educação. Porto Alegre: Mediação Editora, 1995.
CARRETERO, M. Construtivismo e educação. Porto Alegre: ArtMed, 1997.
FONTANA, R. C. Mediação pedagógica na sala de aula. São Paulo: Autores Associados,
1996.
BRUNER, J. S. O processo da educação. 5.ed. São Paulo: Companhia Editora Nacional,
1975.
____________. Actual Minds, Possible Worlds. Cambridge: Harvard University Press,
1986.
67
APÊNDICE A. CÓDIGO FONTE
# start.s
.global
_start
_start:
mov r4, #0x80000000
// Configuring stack to every cpu mode that we will use
cps #0x13
/* Change to supervisor (SVC) mode */
add sp, r4, #0x2400
cps #0x17
/* Change to Abort mode */
add sp, r4, #0x2800
cps #0x12
/* Change to IRQ mode */
add sp, r4, #0x2c00
cps #0x1f
/* Change to system mode */
add sp, r4, #0x3c00
/* Turn on unaligned
mrc p15, #0, r4, c1,
orr r4, #0x400000 /*
mcr p15, #0, r4, c1,
memory access */
c0, #0
1<22 */
c0, #0
/* Jump to memory map initialisation code */
b initsys
68
# initsys.c
/* Virtual memory layout
*
* 0x00000000 - 0x7fffffff (0-2GB) = user process memory
* 0x80000000 - 0xa0ffffff (2GB) = physical memory
*
includes peripherals at 0x20000000 - 0x20ffffff
* 0xc0000000 - 0xefffffff = kernel heap/stack
* 0xf0000000 - 0xffffffff = kernel code
*
* Memory from 0x80000000 upwards won't be accessible to user processes
*/
static unsigned int *initpagetable = (unsigned int * const)0x4000; /* 16K
*/
static unsigned int *kerneldatatable = (unsigned int * const)0x3c00; /* 1K
*/
/* initsys calls main() when it's finished, so we need to tell the compiler
* it's an external symbol
*/
extern void main(void);
/* Memory locations. Defined in linkscript, set during linking */
extern unsigned int _physdatastart, _physbssstart, _physbssend;
extern unsigned int _kstart, _kend;
__attribute__((naked)) void
{
register unsigned int
register unsigned int
register unsigned int
register unsigned int
initsys(void)
x;
pt_addr;
control;
*bss;
/* Save r0-r2 as they contain the start values used by the kernel */
asm volatile("push {r0, r1, r2}");
/* The MMU has two translation tables. Table 0 covers the bottom
* of the address space, from 0x00000000, and deals with between
* 32MB to 4GB of the virtual address space.
* Translation table 1 covers the rest of the memory. For now,
* both tables are set to the same thing, and, by default, table
* 0 manages the entire virtual address space. Later on, the
* table 0 register will be pointed to process-specific tables for
* each process's virtual memory
*/
/*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
Set up translation table
ARM1176JZF-S manual, 6-39
The memory is divided in to 4096 1MB sections. Most of these are
unmapped (resulting in prefetch/data aborts), except
0x8000000-0xa1000000, which are mapped to 0x00000000-0x2a000000
(physical memory and peripherals), and the kernel code and data
Memory privilege is set by APX/AP bits (3 bits in total)
APX is at bit 15 of the sector definition, while AP are at bits
10 and 11.
APX AP
Privileged
1 11 (0x8c00) = read-only
1 01 (0x8400) = read-only
Unprivileged
read-only
no access
69
* 0 10 (0x0800) = read-write
read-only
* 0 01 (0x0400) = read-write
no-access
*
* eXecute Never (XN) is at bit 4 (0x10) - sections with this flag
* cannot be executes, even by privileged processor modes
*
* Bits 0 and 1 identify the table entry type
* 0 or 3 = translation fault (3 is reserved and shouldn't be used)
* 1 = course page table
* 2 = section or supersection
*/
for(x=0; x<4096; x++)
{
if((x >= (0x80000000>>20)) && (x < (0xa1000000>>20)))
{
/* Map physical memory to virtual
* Read/write for priviledged modes, no execute
*/
initpagetable[x] = (x-2048)<<20 | 0x0410 | 2;
}
else
{
/* No entry in table; translation fault */
initpagetable[x] = 0;
}
}
/* Map 0x00000000-0x000fffff into virtual memory at the same address.
* This is temporary: it's where the code is currently running. Once
* initsys has branched tothe kernel at 0xf0000000, it will be
removed
*
* Read/write for privileged mode only
*/
initpagetable[0] = 0<<20 | 0x0400 | 2;
/* Map 1MB at 0xf00000000-0xf00fffff to the kernel code in memory.
* Typically, this is loaded at 0x9000, and appears in virtual memory
* from 0xf0009000. This means we can use a single 1MB entry to
* cover it all (as long as the kernel doesn't get above 1MB...),
* reducing the number of TLB entries
*
* Map as read-only for privileged modes only
*
* It will also map any free memory surrounding the kernel code (eg.
* 0x00000000-0x8fffffff, kernel data). However, as this mapping is
* read-only and only available to the kernel, the potential for harm
* is minimal
*/
initpagetable[3840] = 0<<20 | 0x8400 | 2;
/* 0xc0000000-0xc00fffff is mapped to physical memory by a course
* page table
*
* This maps the kernel data from where it has been loaded in memory
* (after the kernel code, eg. at 0x0001f000) to 0xc0000000
* Only memory in use is mapped (to the next 4K). The rest of the
* table is unmapped.
*/
initpagetable[3072] = 1 | (unsigned int)kerneldatatable;
70
/* Populate kerneldatatable - see ARM1176JZF-S manual, 6-40
*
* APX/AP bits for a page table entry are at bits 9 and 4&5. The
* meaning is the same as for a section entry.
* 0 01 (0x0010) = read-write privileded modes, no access otherwise
*
* bits 0 and 1 determine the page type:
* 0 = unmapped, translation fault
* 1 = large page (64K)
(XN is bit 15)
* 2 = small page (4K), executable (XN is bit 0)
* 3 = small page (4K), not-executable (XN is bit 0)
*
* 256 entries, one for each 4KB in the 1MB covered by the table
*/
for(x=0; x<256; x++)
{
/* &_physbssend is the physical address of the end of the
* kernel data - somewhere between 0x00009000 and 1MB (any
* more than that and this code will need rewriting...)
*/
if(x <= ((unsigned int)&_physbssend >> 12))
kerneldatatable[x] = ((unsigned int)&_physdatastart +
(x<<12)) | 0x0010 | 2;
else
kerneldatatable[x] = 0;
}
/* The .bss section is allocated in physical memory, but its contents
* (all zeroes) are not loaded in with the kernel.
* It needs to be zeroed before it can be used
*/
bss = &_physbssstart;
while(bss<&_physbssend)
{
*bss = 0;
bss++;
}
pt_addr = (unsigned int) initpagetable;
/* Translation table 0 - ARM1176JZF-S manual, 3-57 */
asm volatile("mcr p15, 0, %[addr], c2, c0, 0" : : [addr] "r"
(pt_addr));
/* Translation table 1 */
asm volatile("mcr p15, 0, %[addr], c2, c0, 1" : : [addr] "r"
(pt_addr));
/* Use translation table 0 for everything, for now */
asm volatile("mcr p15, 0, %[n], c2, c0, 2" : : [n] "r" (0));
/* Set Domain 0 ACL to "Client", enforcing memory permissions
* See ARM1176JZF-S manual, 3-64
* Every mapped section/page is in domain 0
*/
asm volatile("mcr p15, 0, %[r], c3, c0, 0" : : [r] "r" (0x1));
/* Read control register */
asm volatile("mrc p15, 0, %[control], c1, c0, 0" : [control] "=r"
(control));
/* Turn on MMU */
control |= 1;
/* Enable ARMv6 MMU features (disable sub-page AP) */
71
control |= (1<<23);
/* Write value back to control register */
asm volatile("mcr p15, 0, %[control], c1, c0, 0" : : [control] "r"
(control));
/* Set the LR (R14) to the address of main(), then pop off r0-r2
* before exiting this function (which doesn't store anything else
* on the stack). The "mov lr" comes first as it's impossible to
* guarantee the compiler wouldn't use one of r0-r2 for %[main]
*/
asm volatile("mov lr, %[main]" : : [main] "r" ((unsigned int)&main)
);
asm volatile("pop {r0, r1, r2}");
asm volatile("bx lr");
}
72
# main.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
"led.h"
"framebuffer.h"
"interrupts.h"
"mailbox.h"
"memory.h"
"memutils.h"
"textutils.h"
"process.h"
"scheduler.h"
// Location of the initial page table in RAM
static unsigned int *initpagetable = (unsigned int *) mem_p2v(0x4000);
void main(unsigned int r0, unsigned int machtype, unsigned int atagsaddr)
{
// Initialise basic memory management
mem_init();
// Initialize led management
led_init();
// Initialize framebuffer (video reserved memory location)
fb_init();
// First boot message
console_write("Welcome to PIqueno\n\n");
// Creating my process in the process table
create_main_process();
// Create three sample
fork("Sample process
fork("Sample process
fork("Sample process
processes with name and address of function
1", &sample_process_1);
2", &sample_process_2);
3", &sample_process_2);
// Configuring iterrupts
interrupts_init();
// Do nothing and wait for scheduler to stop executing me
main_endloop();
}
void main_endloop(void)
{
// Repeatedly halt the CPU and wait for interrupt
halt();
}
73
# interrupts.h
#ifndef INTERRUPTS_H
#define INTERRUPTS_H
extern void interrupts_init(void);
#endif
/* INTERRUPTS_H */
# interrups.c
#include "interrupts.h"
#include
#include
#include
#include
#include
#include
"framebuffer.h"
"led.h"
"memory.h"
"textutils.h"
"scheduler.h"
"syscall.h"
static volatile unsigned int *irqEnable1 = (unsigned int *)
mem_p2v(0x2000b210);
static volatile unsigned int *irqEnable2 = (unsigned int *)
mem_p2v(0x2000b214);
static volatile unsigned int *irqEnableBasic = (unsigned int *)
mem_p2v(0x2000b218);
static volatile unsigned
mem_p2v(0x2000b400);
static volatile unsigned
mem_p2v(0x2000b404);
static volatile unsigned
mem_p2v(0x2000b408);
static volatile unsigned
mem_p2v(0x2000b40c);
int *armTimerLoad = (unsigned int *)
int *armTimerValue = (unsigned int *)
int *armTimerControl = (unsigned int *)
int *armTimerIRQClear = (unsigned int *)
/* Interrupt vectors called by the CPU. Needs to be aligned to 32 bits as
the
* bottom 5 bits of the vector address as set in the control coprocessor
must
* be zero
*
* The RESET vector is set to bad_exception. On CPU reset the interrupt
vectors
* are set back to 0x00000000, so it won't be used. Any attempt to call
this
* vector is clearly an error. Also, resetting the Pi will reset VideoCore,
* and reboot.
*/
__attribute__ ((naked, aligned(32))) static void interrupt_vectors(void)
{
asm volatile("b bad_exception\n"
// Reset
"b bad_exception\n"
// Undefined
"b interrupt_swi\n"
// Software
interrupt
"b interrupt_prefetch_abort \n"
// Prefech abort
interrupt
"b interrupt_data_abort \n"
// Data abort
interrupt
"b bad_exception;\n"
// Unused vector
"b interrupt_irq \n"
// IRQ interrupt
"b bad_exception\n"
// Fast
Interrupt (FIQ), not used
74
);
}
// Unhandled exceptions, halt
__attribute__ ((naked)) void bad_exception(void)
{
console_write("Bad exception. System halted.");
while(1);
}
// Software interrupt
__attribute__ ((interrupt ("SWI"))) void interrupt_swi(void)
{
unsigned int addr;
unsigned int swi;
// Read link register into addr - contains the address of the
instruction after the SWI
asm volatile("mov %[addr], lr" : [addr] "=r" (addr) );
addr -= 4;
// Bottom 24 bits of the SWI instruction are the SWI number
swi = *((unsigned int *)addr) & 0x00ffffff;
// Debug message
console_write("\n");
console_write(COLOUR_PUSH FG_GREEN "SWI call. Address: 0x");
console_write(tohex(addr, 4));
console_write(" SWI number ");
console_write(todec(swi, 0));
console_write(COLOUR_POP "\n");
// Changing processor mode
asm volatile("cps #0x1f");
// Handle syscall
syscall(swi);
}
// IRQ
__attribute__ ((interrupt ("IRQ"))) void interrupt_irq(void)
{
// This function starts on IRQ mode
// Push all registers into the IRQ mode stack (R13)
asm volatile("push {R0-R12}");
// Put LR register of IRQ mode (PC of interrupted process) on R0
asm volatile("MOV R0, LR");
// Change to system mode
asm volatile("cps #0x1f");
// Push R0 (interrupted PC) to the system mode stack
asm volatile("push {R0}");
// Return to IRQ mode
asm volatile("cps #0x12");
75
// Pop all registers again
asm volatile("pop {R0-R12}");
// Return to system mode
asm volatile("cps #0x1f");
// Push all registers into the system mode stack
asm volatile("push {R0-R12}");
// Push the interrupted LR to system mode stack
asm volatile("push {LR}");
// Copy the processor status to R0
asm volatile("MRS R0, SPSR");
// Push the processor status to system mode stack
asm volatile("push {R0}");
// Return to IRQ mode
asm volatile("cps #0x12");
// Copy LR to R0
asm volatile("MOV R0, LR");
// Back to system mode
asm volatile("cps #0x1f");
unsigned long pc;
unsigned long stack_pointer;
// Getting pc and stack just to debug
asm volatile ("MOV %0, R0\n\t" : "=r" (pc) );
asm volatile ("MOV %0, SP\n\t" : "=r" (stack_pointer) );
// Invert led to inform context switch activity
led_invert();
// Jump to scheduler to do the context switch
schedule_timeout(stack_pointer, pc);
}
__attribute__ ((interrupt ("ABORT"))) void interrupt_data_abort(void)
{
register unsigned int addr, far;
asm volatile("mov %[addr], lr" : [addr] "=r" (addr) );
/* Read fault address register */
asm volatile("mrc p15, 0, %[addr], c6, c0, 0": [addr] "=r" (far) );
console_write("Data abort!\n");
console_write("Instruction address: 0x");
/* addr = lr, but the very start of the abort routine does
* sub lr, lr, #4
* lr = address of aborted instruction, plus 8
*/
console_write(tohex(addr-4, 4));
76
console_write(" fault address: 0x");
console_write(tohex(far, 4));
console_write("\n");
/* Routine terminates by returning to LR-4, which is the instruction
* after the aborted one
* GCC doesn't properly deal with data aborts in its interrupt
* handling - no option to return to the failed instruction
*/
asm volatile("cps #0x1f");
asm volatile("cpsid aif");
}
// Return to this function after a prefetch abort
extern void main_endloop(void);
__attribute__ ((interrupt ("ABORT"))) void interrupt_prefetch_abort(void)
{
register unsigned int addr;
asm volatile("mov %[addr], lr" : [addr] "=r" (addr) );
console_write("Prefetch abort!\n");
console_write("Instruction address: 0x");
/* lr = address of aborted instruction, plus 4
* addr = lr, but the very start of the abort routine does
* sub lr, lr, #4
*/
console_write(tohex(addr, 4));
console_write("\n");
/* Set the return address to be the function main_endloop(), by
* putting its address into the program counter
*
* THIS IS JUST A TEST - you can't normally do this as it doesn't
* restore the registers or put the stack pointer back where it was,
* so repeated aborts will overflow the stack.
*/
asm volatile("movs pc, %[addr]" : :
[addr] "r" ((unsigned int)(&main_endloop)) );
/* Doesn't reach this point */
}
/* Initialise the interrupts
*
* Enable the ARM timer interrupt
*/
void interrupts_init(void)
{
// Set interrupt base register
asm volatile("mcr p15, 0, %[addr], c12, c0, 0" : : [addr] "r"
(&interrupt_vectors));
// Turn on interrupts
asm volatile("cpsie i");
// Enable ARM timer IRQ
*irqEnableBasic = 0x00000001;
// Interrupt every x * 256 (prescaler) timer ticks
77
*armTimerLoad = 0x00000400;
// Timer enabled
*armTimerControl = 0x000000aa;
}
void timer_reset(void)
{
*armTimerIRQClear = 0;
}
78
# scheduler.h
#ifndef SCHEDULER_H
#define
SCHEDULER_H
#include "process.h"
void
void
void
void
void
create_main_process();
fork(char * name, unsigned long addr);
schedule_timeout(unsigned long stack_pointer, unsigned long pc);
terminate_process();
halt();
#endif
/* SCHEDULER_H */
# scheduler.c
#include
#include
#include
#include
static
static
static
static
"process.h"
"framebuffer.h"
"textutils.h"
"interrupts.h"
process process_list[1024];
unsigned int stack_base;
unsigned int process_count = 0;
unsigned int active_process_index;
extern void main_endloop();
extern void timer_reset();
void terminate_process() {
// Set it to terminated status, so the scheduler will ignore it
process_list[active_process_index].status =
PROCESS_STATUS_TERMINATED;
}
// Function to create the main process
void create_main_process() {
// Creating process
process main_process;
main_process.id = process_count;
main_process.name = "Main";
main_process.pc = &main_endloop;
main_process.times_loaded = 1;
main_process.status = PROCESS_STATUS_ZOMBIE;
// Gets the actual stack address
unsigned int stack_pointer;
asm volatile ("MOV %0, SP\n\t" : "=r" (stack_pointer) );
// Use it as base address processes stack pointer
stack_base = stack_pointer;
// Output the stack address
console_write("Main stack is 0x");
console_write(tohex(stack_pointer, 4));
console_write("\n");
// Saving the process in the process table
79
process_list[process_count] = main_process;
// Set it as the current active process
active_process_index = process_count;
// Increments the process counter
process_count++;
}
/*
* Procedure to fork this process, creating a new one, pointing the pc
* to the memory address of the desired procedure
*/
void fork(char * name, unsigned long * pc) {
process fork_process;
// Basic memory organization to get new stack addr. just add 1024
bytes from the main stack
unsigned int * forked_stack_pointer = stack_base + (process_count *
1024);
console_write("Forked stack is 0x");
console_write(tohex(forked_stack_pointer, 4));
console_write("\n");
fork_process.id = process_count;
fork_process.name = name;
fork_process.pc = pc;
fork_process.parent = active_process_index;
fork_process.times_loaded = 0;
fork_process.stack_pointer = forked_stack_pointer;
fork_process.status = PROCESS_STATUS_WAITING;
process_list[process_count] = fork_process;
process_count++;
}
/*
* Function to get the next ready process in the list
*/
int next_waiting_process_index() {
// Start in the active index
int next_process_index = active_process_index;
// Do this while the actual process isnt in the waiting status and
not reach the actual running process
do {
next_process_index++;
// Just rewind the list
if (next_process_index == process_count) {
next_process_index = 0;
}
} while ((process_list[next_process_index].status !=
PROCESS_STATUS_WAITING) && (next_process_index != active_process_index));
// If the found process isnt waiting
80
if (process_list[next_process_index].status !=
PROCESS_STATUS_WAITING) {
return -1;
}
return next_process_index;
}
/*
* Just keep the processor busy
*/
void halt() {
while(1);
}
/*
* Procedure coming from the IRQ interrupt, to change the running process
*/
void schedule_timeout(unsigned long stack_pointer, unsigned long pc) {
// Saving stack and pc
process_list[active_process_index].stack_pointer = stack_pointer;
process_list[active_process_index].pc = pc;
// Updating process status to waiting
if (process_list[active_process_index].status ==
PROCESS_STATUS_RUNNING) {
process_list[active_process_index].status =
PROCESS_STATUS_WAITING;
}
console_write("\n");
console_write("\n");
console_write("Schedule timeout. Current active pid is ");
console_write(todec(process_list[active_process_index].id, 0));
console_write(" with name ");
console_write(process_list[active_process_index].name);
console_write(". Switching to next process.\n");
console_write("Saving stack...");
console_write(" stack saved, was 0x");
console_write(tohex(process_list[active_process_index].stack_pointer,
4));
console_write("\n");
console_write("Saving pc...");
console_write(" pc saved, was 0x");
console_write(tohex(process_list[active_process_index].pc, 4));
console_write("\n");
// Get next process id
int next_process = next_waiting_process_index();
// If -1, halt
if (next_process < 0) {
console_write("No more waiting processes, halting.");
halt();
}
// Updating next running process
active_process_index = next_process;
81
// Increasing statistics, updating status to running
process_list[active_process_index].times_loaded++;
process_list[active_process_index].status = PROCESS_STATUS_RUNNING;
console_write("Restoring stack 0x");
console_write(tohex(process_list[active_process_index].stack_pointer,
4));
console_write("\n");
console_write("Restoring pc 0x");
console_write(tohex(process_list[active_process_index].pc, 4));
console_write("\n");
// Assembly to load the sp with the new process stack address
asm volatile("MOV SP, %[addr]" : : [addr] "r" ((unsigned long
)(process_list[active_process_index].stack_pointer)) );
// If its is not the first time
if (process_list[active_process_index].times_loaded > 1) {
timer_reset();
// Pops registers
asm volatile("pop
asm volatile("MSR
asm volatile("pop
asm volatile("pop
from the stack
{R0}");
SPSR_cxsf, R0");
{LR}");
{R0 - R12}");
// Turn on interrupt again
asm volatile("cpsie i");
// Pops the last register into PC to resume processing
asm volatile("pop {PC}");
} else {
// Push the first pc address into the stack
unsigned long addr = (unsigned long
)(process_list[active_process_index].pc);
asm volatile("MOV R0, %[addr]" : : [addr] "r" (addr) );
asm volatile("push {R0}");
timer_reset();
// Turn on interrupt again
asm volatile("cpsie i");
// Pops the last register into PC to resume processing
asm volatile("pop {PC}");
}
}
82
# process.h
#ifndef PROCESS_H
#define
PROCESS_H
#define PROCESS_STATUS_RUNNING
#define PROCESS_STATUS_WAITING
processor
#define PROCESS_STATUS_ZOMBIE
to terminate
#define PROCESS_STATUS_TERMINATED
#define PROCESS_STATUS_ABORTED
0
1
// In the processor
// Waiting its time on the
2
// Dead, but waiting child process
3
// Terminated gracefully
4
// Terminated abnormally
typedef struct {
unsigned int id;
char *
name;
unsigned int parent;
unsigned long
stack_pointer;
unsigned long
pc;
unsigned int times_loaded;
unsigned int status;
} process;
extern void sample_process_1();
extern void sample_process_2();
#endif
/* PROCESS_H */
# process.c
#include "textutils.h"
#include "framebuffer.h"
#include "scheduler.h"
// Just some counting for easy debug on the screen. Simulate user process.
void sample_process_1() {
console_write("Starting process 1 ");
int to = 300;
int i, j;
for (i=0; i<to; i++) {
console_write(todec(i,0));
console_write(" ");
for (j=0; j<to*to; j++) {
asm volatile("NOP");
}
}
// Call software interrupt #0 (terminate)
asm volatile("SWI #0");
}
// Just some counting for easy debug on the screen. Simulate user process.
void sample_process_2() {
console_write("Starting process 2 ");
int to = 300;
83
int i, j;
for (i=0; i<to; i++) {
console_write(todec(i,0));
console_write(" ");
for (j=0; j<to*to; j++) {
asm volatile("NOP");
}
}
// Call software interrupt #0 (terminate)
asm volatile("SWI #0");
}
84
# syscall.h
#ifndef SYSCALL_H
#define
SYSCALL_H
#define SYSCALL_TERMINATE_PROCESS
0
void syscall(unsigned int swi);
#endif
/* SYSCALL_H */
# syscall.c
#include
#include
#include
#include
"scheduler.h"
"syscall.h"
"framebuffer.h"
"textutils.h"
/*
* Procedure to handle software interrupts
*/
void syscall(unsigned int swi) {
console_write("Handling syscall: ");
console_write(todec(swi, 0));
console_write("\n");
switch (swi) {
case SYSCALL_TERMINATE_PROCESS:
console_write("Invoking syscall terminate_process()");
console_write("\n");
// Calling the right intra-kernel procedure
terminate_process();
break;
}
console_write("Turning interrupt on again");
console_write("\n");
// Turn on interrupt again
asm volatile("cpsie i");
// Wait for timer interrupt. Probably not the best thing to do.
halt();
}
85
# barrier.h
#ifndef BARRIER_H
#define BARRIER_H
/*
* Data memory barrier
* No memory access after the DMB can run until all memory accesses before
it
* have completed
*/
#define dmb() asm volatile \
("mcr p15, #0, %[zero], c7, c10, #5" : : [zero] "r" (0) )
/*
* Data synchronisation barrier
* No instruction after the DSB can run until all instructions before it
have
* completed
*/
#define dsb() asm volatile \
("mcr p15, #0, %[zero], c7, c10, #4" : : [zero] "r" (0) )
/*
* Clean and invalidate entire cache
* Flush pending writes to main memory
* Remove all data in data cache
*/
#define flushcache() asm volatile \
("mcr p15, #0, %[zero], c7, c14, #0" : : [zero] "r" (0) )
#endif
/* BARRIER_H */
86
# memory.h
#ifndef MEMORY_H
#define MEMORY_H
/* Convert a virtual address to a physical one by following the page tables
* Returns physical address, or 0xffffffff if the virtual address does not
map
*/
extern unsigned int mem_v2p(unsigned int);
/* Convert a physical address to a virtual one - essentially, just add
* 0x80000000 to it
*/
#define mem_p2v(X) (X+0x80000000)
extern void mem_init(void);
#endif /* MEMORY_H */
# memory.c
#include "memory.h"
/* Virtual memory layout
*
* 0x00000000 - 0x7fffffff
* 0x80000000 - 0xa0ffffff
*
includes peripherals
* 0xc0000000 - 0xefffffff
* 0xf0000000 - 0xffffffff
*/
(0-2GB) = user process memory
= physical memory
at 0x20000000 - 0x20ffffff
= kernel data
= kernel code
/* Need to access the page table, etc as physical memory */
static unsigned int *pagetable = (unsigned int * const) mem_p2v(0x4000); /*
16k */
/* Last used location in physical RAM */
extern unsigned int _physbssend;
/* Start of kernel in physical RAM */
extern unsigned int _highkernelload;
/* Convert a virtual address to a physical one by following the page tables
* Returns physical address, or 0xffffffff if the virtual address does not
map
* See ARM1176-TZJS technical reference manual, page 6-39 (6.11.2)
*/
unsigned int mem_v2p(unsigned int virtualaddr)
{
unsigned int pt_data = pagetable[virtualaddr >> 20];
unsigned int cpt_data, physaddr;
if((pt_data & 3)==0 || (pt_data & 3)==3)
{
/* Nothing mapped */
return 0xffffffff;
}
if((pt_data & 3)==2)
{
/* (Super)Section */
physaddr = pt_data & 0xfff00000;
87
if(pt_data & (1<<18))
{
/* 16MB Supersection */
physaddr += virtualaddr & 0x00ffffff;
}
else
{
/* 1MB Section */
physaddr += virtualaddr & 0x000fffff;
}
return physaddr;
}
/* Coarse page table */
cpt_data = ((unsigned int *)(0x80000000 + (pt_data &
0xfffffc00)))[(virtualaddr>>12)&0xff] ;
if((cpt_data & 3) == 0)
{
/* Nothing mapped */
return 0xffffffff;
}
if(cpt_data & 2)
{
/* Small (4k) page */
return (cpt_data & 0xfffff000) + (virtualaddr & 0xfff);
}
/* Large (64k) page */
return (cpt_data & 0xffff0000) + (virtualaddr & 0xffff);
}
/* mem_p2v is a simple macro defined in memory.h which adds 0x80000000 to
an
* address, to put the physical memory address (0x00000000-0x20ffffff) into
* the corresponding mapped area of virtual memory (0x80000000-0xa0ffffff)
*/
/* Translation table 0 - covers the first 64 MB, for now
* Needs to be aligned to its size (ie 64*4 bytes)
*/
unsigned int pagetable0[64]
__attribute__ ((aligned (256)));
/* Initialise memory - actually, there's not much to do now, since initsys
* covers most of it. It just sets up a pagetable for the first 64MB of RAM
* (all unmapped)
*/
void mem_init(void)
{
unsigned int x;
unsigned int pt0_addr;
/* Translation table 0 - covers the first 64 MB, for now
* Currently nothing mapped in it.
*/
for(x=0; x<64; x++)
{
pagetable0[x] = 0;
88
}
/* Get physical address of pagetable0 */
pt0_addr = mem_v2p((unsigned int) &pagetable0);
/* Use translation table 0 up to 64MB */
asm volatile("mcr p15, 0, %[n], c2, c0, 2" : : [n] "r" (6));
/* Translation table 0 - ARM1176JZF-S manual, 3-57 */
asm volatile("mcr p15, 0, %[addr], c2, c0, 0" : : [addr] "r"
(pt0_addr));
/* Invalidate the translation lookaside buffer (TLB)
* ARM1176JZF-S manual, p. 3-86
*/
asm volatile("mcr p15, 0, %[data], c8, c7, 0" : : [data] "r" (0));
}
89
# memutils.h
#ifndef MEMUTILS_H
#define MEMUTILS_H
/* Clear length bytes of memory (set to 0) starting at address */
extern void memclr(void *address, unsigned int length);
/* Move length bytes from src to dest. Memory areas may overlap */
extern void *memmove(void *dest, const void *src, unsigned int length);
#endif
/* MEMUTILS_H */
# memutils.c
/* Various memory utilities */
#include "memutils.h"
/* Clear (set to 0) length bytes of memory starting at address
*/
void memclr(void *address, unsigned int length)
{
register unsigned int addr = (unsigned int)address;
/* If the start address is unaligned, fill in the first 1-3 bytes
* until it is
*/
while((addr & 3) && length)
{
*((unsigned char *)addr) = 0;
addr++;
length--;
}
/* Fill in the remaining 32-bit word-aligned memory locations */
while(length & 0xfffffffc)
{
*((unsigned int *)addr) = 0;
addr+=4;
length-=4;
}
/* Deal with the remaining 1-3 bytes, if any */
while(length)
{
addr++;
length--;
*((unsigned char *)addr) = 0;
}
}
/* Move length bytes from src to dest. Memory areas may overlap
* Four possibilities:
* ..[...src...]..[...dest...]..
-- non-overlapping
* ..[...dest...]..[...src...]..
-- non-overlapping
* ..[...src..[.]..dest...]..
-- overlapping
* ..[...dest..[.]..src...]..
-- overlapping
* The first two can be dealt with by copying from either end of the source
* block
* The third (overlapping, source first) by copying from the end of the
block
* back to the start
90
* The fourth (overlapping, destination first) by copying from the start of
the
* source block to the end
*
* Returns the address of the destination memory area
*/
void *memmove(void *dest, const void *src, unsigned int length)
{
/* Turn destination and source pointers into integers for easier
* calculations
*/
register unsigned int d = (unsigned int)dest;
register unsigned int s = (unsigned int)src;
if(!length)
return dest;
/* Assume the memory blocks are word aligned. Most will be, and the
* CPU can deal with unaligned accesses if necessary (as long as it
* is configured to do so)
*/
if(d>s && d<(s+length))
{
/* Destination starts inside source area - work backwards */
/* If length isn't a multiple of 4 bytes, copy the last 1-3
* bytes first
*/
while(length & 3)
{
length--;
((unsigned char *)d)[length] = ((unsigned char
*)s)[length];
}
/* Copy everything else as 32-bit words. If one or both of the
* memory areas aren't aligned, this will cause unaligned
* reads. Inefficient, but less so than doing everything as
* a series of bytes
*/
while(length)
{
length-=4;
*((unsigned int *)d+length) = *((unsigned int
*)s+length);
}
}
else
{
/* Source starts inside destination area - working forwards
* is fine - or two areas don't overlap (or they overlap
* exactly, but that's an unlikely edge case)
*
* Copy as much as possible as 32-bit words. See above for
* alignment issues
*/
while(length & 0xfffffffc)
{
*((unsigned int *)d) = *((unsigned int *)s);
d+=4;
s+=4;
length-=4;
91
}
/* Deal with 1-3 remaining bytes, if applicable */
while(length)
{
*((unsigned char *)d) = *((unsigned char *)s);
d++;
s++;
length--;
}
}
return dest;
}
92
# framebuffer.h
#ifndef FRAMEBUFFER_H
#define FRAMEBUFFER_H
extern void fb_init(void);
extern void console_write(char *text);
/* Control characters for the console */
#define FG_RED "\001"
#define FG_GREEN "\002"
#define FG_BLUE "\003"
#define FG_YELLOW "\004"
#define FG_MAGENTA "\005"
#define FG_CYAN "\006"
#define FG_WHITE "\007"
#define FG_BLACK "\010"
#define FG_HALF "\011"
#define COLOUR_PUSH "\013"
#define COLOUR_POP "\014"
#define
#define
#define
#define
#define
#define
#define
#define
#define
#endif
BG_RED "\021"
BG_GREEN "\022"
BG_BLUE "\023"
BG_YELLOW "\024"
BG_MAGENTA "\025"
BG_CYAN "\026"
BG_WHITE "\027"
BG_BLACK "\030"
BG_HALF "\031"
/* FRAMEBUFFER_H */
# framebuffer.c
#include "framebuffer.h"
#include "led.h"
#include "mailbox.h"
#include "memory.h"
#include "memutils.h"
#include "textutils.h"
/* SAA5050 (teletext) character definitions */
#include "teletext.h"
/* Framebuffer initialisation failure codes
* If the FB can't be initialised, one of the following numbers will be
* flashed on the OK LED
*/
/* Mailbox call to get screen resolution failed */
#define FBFAIL_GET_RESOLUTION
1
/* Mailbox call returned bad resolution */
#define FBFAIL_GOT_INVALID_RESOLUTION
2
/* Mailbox call to setup FB failed */
#define FBFAIL_SETUP_FRAMEBUFFER
3
/* Setup call FB returned an invalid list of response tags */
#define FBFAIL_INVALID_TAGS
4
/* Setup FB call returned an invalid response for the framebuffer tag */
#define FBFAIL_INVALID_TAG_RESPONSE 5
/* Setup FB call returned an invalid address/size */
#define FBFAIL_INVALID_TAG_DATA
6
93
/* Read
#define
/* Read
#define
FB pitch call returned an invalid response */
FBFAIL_INVALID_PITCH_RESPONSE
7
FB pitch call returned an invalid pitch value */
FBFAIL_INVALID_PITCH_DATA 8
/* Character cells are 6x10 */
#define CHARSIZE_X
6
#define CHARSIZE_Y
10
/* Screen parameters set in fb_init() */
static unsigned int screenbase, screensize;
static unsigned int fb_x, fb_y, pitch;
/* Max x/y character cell */
static unsigned int max_x, max_y;
/* Framebuffer initialisation failed. Can't display an error, so flashing
* the OK LED will have to do
*/
static void fb_fail(unsigned int num)
{
while(1)
output(num);
}
/* Initialise the framebuffer */
void fb_init(void)
{
unsigned int var;
unsigned int count;
unsigned int physical_screenbase;
/* Storage space for the buffer used to pass information between the
* CPU and VideoCore
* Needs to be aligned to 16 bytes as the bottom 4 bits of the
address
* passed to VideoCore are used for the mailbox number
*/
volatile unsigned int mailbuffer[256] __attribute__((aligned (16)));
/* Physical memory address of the mailbuffer, for passing to VC */
unsigned int physical_mb = mem_v2p((unsigned int)mailbuffer);
/* Get the display size */
mailbuffer[0] = 8 * 4;
mailbuffer[1] = 0;
mailbuffer[2] = 0x40003;
mailbuffer[3] = 8;
mailbuffer[4] = 0;
mailbuffer[5] = 0;
mailbuffer[6] = 0;
mailbuffer[7] = 0;
// Total size
// Request
// Display size
// Buffer size
// Request size
// Space for horizontal resolution
// Space for vertical resolution
// End tag
writemailbox(8, physical_mb);
var = readmailbox(8);
/* Valid response in data structure */
if(mailbuffer[1] != 0x80000000)
fb_fail(FBFAIL_GET_RESOLUTION);
fb_x = mailbuffer[5];
94
fb_y = mailbuffer[6];
/* If both fb_x and fb_y are both zero, assume we're running on the
* qemu Raspberry Pi emulation (which doesn't return a screen size
* at this point), and request a 640x480 screen
*/
if(fb_x==0 && fb_y==0)
{
fb_x = 640;
fb_y = 480;
}
if(fb_x==0 || fb_y==0)
fb_fail(FBFAIL_GOT_INVALID_RESOLUTION);
// Setup screen
unsigned int c = 1;
mailbuffer[c++] = 0;
// Request
mailbuffer[c++]
mailbuffer[c++]
mailbuffer[c++]
mailbuffer[c++]
mailbuffer[c++]
=
=
=
=
=
0x00048003; // Tag id (set physical size)
8;
// Value buffer size (bytes)
8;
// Req. + value length (bytes)
fb_x;
// Horizontal resolution
fb_y;
// Vertical resolution
mailbuffer[c++]
mailbuffer[c++]
mailbuffer[c++]
mailbuffer[c++]
mailbuffer[c++]
=
=
=
=
=
0x00048004; // Tag id (set virtual size)
8;
// Value buffer size (bytes)
8;
// Req. + value length (bytes)
fb_x;
// Horizontal resolution
fb_y;
// Vertical resolution
mailbuffer[c++]
mailbuffer[c++]
mailbuffer[c++]
mailbuffer[c++]
=
=
=
=
0x00048005; // Tag id (set depth)
4;
// Value buffer size (bytes)
4;
// Req. + value length (bytes)
16;
// 16 bpp
mailbuffer[c++]
mailbuffer[c++]
mailbuffer[c++]
mailbuffer[c++]
mailbuffer[c++]
=
=
=
=
=
0x00040001; // Tag id (allocate framebuffer)
8;
// Value buffer size (bytes)
4;
// Req. + value length (bytes)
16;
// Alignment = 16
0;
// Space for response
mailbuffer[c++] = 0;
// Terminating tag
mailbuffer[0] = c*4;
// Buffer size
writemailbox(8, physical_mb);
var = readmailbox(8);
/* Valid response in data structure */
if(mailbuffer[1] != 0x80000000)
fb_fail(FBFAIL_SETUP_FRAMEBUFFER);
count=2;
/* First tag */
while((var = mailbuffer[count]))
{
if(var == 0x40001)
break;
95
/* Skip to next tag
* Advance count by 1 (tag) + 2 (buffer size/value size)
*
+ specified buffer size
*/
count += 3+(mailbuffer[count+1]>>2);
if(count>c)
fb_fail(FBFAIL_INVALID_TAGS);
}
/* 8 bytes, plus MSB set to indicate a response */
if(mailbuffer[count+2] != 0x80000008)
fb_fail(FBFAIL_INVALID_TAG_RESPONSE);
/* Framebuffer address/size in response */
physical_screenbase = mailbuffer[count+3];
screensize = mailbuffer[count+4];
if(physical_screenbase == 0 || screensize == 0)
fb_fail(FBFAIL_INVALID_TAG_DATA);
/* physical_screenbase is the address of the screen in RAM
* screenbase needs to be the screen address in virtual memory
*/
screenbase=mem_p2v(physical_screenbase);
/* Get the framebuffer pitch
mailbuffer[0] = 7 * 4;
mailbuffer[1] = 0;
mailbuffer[2] = 0x40008;
mailbuffer[3] = 4;
mailbuffer[4] = 0;
mailbuffer[5] = 0;
mailbuffer[6] = 0;
(bytes per line) */
// Total size
// Request
// Display size
// Buffer size
// Request size
// Space for pitch
// End tag
writemailbox(8, physical_mb);
var = readmailbox(8);
/* 4 bytes, plus MSB set to indicate a response */
if(mailbuffer[4] != 0x80000004)
fb_fail(FBFAIL_INVALID_PITCH_RESPONSE);
pitch = mailbuffer[5];
if(pitch == 0)
fb_fail(FBFAIL_INVALID_PITCH_DATA);
/* Need to set up max_x/max_y before using console_write */
max_x = fb_x / CHARSIZE_X;
max_y = fb_y / CHARSIZE_Y;
}
/* Current console text cursor position (ie. where the next character will
* be written
*/
static int consx = 0;
static int consy = 0;
/* Current fg/bg colour */
static unsigned short int fgcolour = 0xffff;
96
static unsigned short int bgcolour = 0;
/* A small stack to allow temporary colour changes in text */
static unsigned int colour_stack[] = { 0, 0, 0, 0, 0, 0, 0, 0 };
static unsigned int colour_sp = 8;
/* Move to a new line, and, if at the bottom of the screen, scroll the
* framebuffer 1 character row upwards, discarding the top row
*/
static void newline()
{
unsigned int source;
/* Number of bytes in a character row */
register unsigned int rowbytes = CHARSIZE_Y * pitch;
consx = 0;
if(consy<(max_y-1))
{
consy++;
return;
}
/* Copy a screen's worth of data (minus 1 character row) from the
* second row to the first
*/
/* Calculate the address to copy the screen data from */
source = screenbase + rowbytes;
memmove((void *)screenbase, (void *)source, (max_y-1)*rowbytes);
/* Clear last line on screen */
memclr((void *)(screenbase + (max_y-1)*rowbytes), rowbytes);
}
/* Write null-terminated text to the console
* Supports control characters (see framebuffer.h) for colour and newline
*/
void console_write(char *text)
{
volatile unsigned short int *ptr;
unsigned int row, addr;
int col;
unsigned char ch;
/* Double parentheses to silence compiler warnings about
* assignments as boolean values
*/
while((ch = (unsigned char)*text))
{
text++;
/* Deal with control codes */
switch(ch)
{
case 1: fgcolour = 0b1111100000000000;
case 2: fgcolour = 0b0000011111100000;
case 3: fgcolour = 0b0000000000011111;
case 4: fgcolour = 0b1111111111100000;
case 5: fgcolour = 0b1111100000011111;
case 6: fgcolour = 0b0000011111111111;
continue;
continue;
continue;
continue;
continue;
continue;
97
case 7: fgcolour = 0b1111111111111111; continue;
case 8: fgcolour = 0b0000000000000000; continue;
/* Half brightness */
case 9: fgcolour = (fgcolour >> 1) & 0b0111101111101111;
continue;
case 10: newline(); continue;
case 11: /* Colour stack push */
if(colour_sp)
colour_sp--;
colour_stack[colour_sp] =
fgcolour | (bgcolour<<16);
continue;
case 12: /* Colour stack pop */
fgcolour = colour_stack[colour_sp] & 0xffff;
bgcolour = colour_stack[colour_sp] >> 16;
if(colour_sp<8)
colour_sp++;
continue;
case 17: bgcolour = 0b1111100000000000; continue;
case 18: bgcolour = 0b0000011111100000; continue;
case 19: bgcolour = 0b0000000000011111; continue;
case 20: bgcolour = 0b1111111111100000; continue;
case 21: bgcolour = 0b1111100000011111; continue;
case 22: bgcolour = 0b0000011111111111; continue;
case 23: bgcolour = 0b1111111111111111; continue;
case 24: bgcolour = 0b0000000000000000; continue;
/* Half brightness */
case 25: bgcolour = (bgcolour >> 1) & 0b0111101111101111;
continue;
}
/* Unknown control codes, and anything >127, get turned into
* spaces. Anything >=32 <=127 gets 32 subtracted from it to
* turn it into a value between 0 and 95, to index into the
* character definitions table
*/
if(ch<32)
{
ch=0;
}
else
{
if(ch>127)
ch=0;
else
ch-=32;
}
/* Plot character onto screen
*
* CHARSIZE_Y and CHARSIZE_X are the size of the block the
* character occupies. The character itself is one pixel
* smaller in each direction, and is located in the upper left
* of the block
*/
for(row=0; row<CHARSIZE_Y; row++)
{
addr = (row+consy*CHARSIZE_Y)*pitch + consx*CHARSIZE_X*2;
for(col=(CHARSIZE_X-2); col>=0; col--)
{
98
ptr = (unsigned short int *)(screenbase+addr);
addr+=2;
if(row<(CHARSIZE_Y-1) && (teletext[ch][row] &
(1<<col)))
*ptr = fgcolour;
else
*ptr = bgcolour;
}
ptr = (unsigned short int *)(screenbase+addr);
*ptr = bgcolour;
}
if(++consx >=max_x)
{
newline();
}
}
}
99
# textutils.h
#ifndef TEXTUTILS_H
#define TEXTUTILS_H
extern char *tohex(unsigned int value, unsigned int size);
extern char *todec(unsigned int value, int leading);
#endif
/* TEXTUTILS_H */
# textutils.c
#include "textutils.h"
/* Convert an unsigned value to hex (without the trailing "0x")
* size = size in bytes (only 1, 2 or 4 permitted)
*/
char *tohex(unsigned int value, unsigned int size)
{
static char buffer[9];
unsigned int offset;
unsigned char ch;
if(size!=1 && size!=2 && size!=4)
return "error";
offset=size*2;
buffer[offset] = 0;
while(offset--)
{
ch = 48 + (value & 15);
if(ch>=58)
ch+=7;
buffer[offset] = ch;
value = value >> 4;
}
return buffer;
}
/* Convert unsigned value to decimal
* leading = 0 - no leading spaces/zeroes
* leading >0 - number of leading zeroes
* leading <0 - number (when +ve) of leading spaces
*/
char *todec(unsigned int value, int leading)
{
/* Biggest number is 4294967295 (10 digits) */
static char buffer[11];
static char leadchar;
unsigned int offset = 10;
unsigned char ch;
if(leading <0)
{
leading = -leading;
leadchar = ' ';
100
}
else
{
leadchar = '0';
}
if(leading>10)
return "error";
buffer[offset] = 0;
while(value || (offset == 10))
{
offset--;
leading--;
ch = 48 + (value % 10);
buffer[offset] = ch;
value = value/10;
}
while(leading>0)
{
offset--;
leading--;
buffer[offset] = leadchar;
}
return &buffer[offset];
}
101
# teletext.h
// Character definitions from SAA5050 datasheet
// Each character is a 5x9 bit matrix
// 9 rows of 5-bit numbers
static unsigned char teletext[][9] = {
{ 0,0,0,0,0,0,0,0,0 },
// space
{ 4,4,4,4,4,0,4,0,0 },
// !
{ 10,10,10,0,0,0,0,0,0 },
// "
{ 6,9,8,28,8,8,31,0,0 },
// £
# in ASCII
{ 14,21,20,14,5,21,14,0,0 }, // $
{ 24,25,2,4,8,19,3,0,0 },
// %
{ 8,20,20,8,21,18,13,0,0 },
// &
{ 4,4,4,0,0,0,0,0,0 },
// '
{ 2,4,8,8,8,4,2,0,0 },
// (
{ 8,4,2,2,2,4,8,0,0 },
// )
{ 4,21,14,4,14,21,4,0,0 },
// *
{ 0,4,4,31,4,4,0,0,0 },
// +
{ 0,0,0,0,0,4,4,8,0 },
// ,
{ 0,0,0,14,0,0,0,0,0 },
// { 0,0,0,0,0,0,4,0,0 },
// .
{ 0,1,2,4,8,16,0,0,0 },
// /
{ 4,10,17,17,17,10,4,0,0 },
// 0
{ 4,12,4,4,4,4,14,0,0 },
// 1
{ 14,17,1,6,8,16,31,0,0 },
// 2
{ 31,1,2,6,1,17,14,0,0 },
// 3
{ 2,6,10,18,31,2,2,0,0 },
// 4
{ 31,16,30,1,1,17,14,0,0 },
// 5
{ 6,8,16,30,17,17,14,0,0 },
// 6
{ 31,1,2,4,8,8,8,0,0 },
// 7
{ 14,17,17,14,17,17,14,0,0 }, // 8
{ 14,17,17,15,1,2,12,0,0 },
// 9
{ 0,0,4,0,0,0,4,0,0 },
// :
{ 0,0,4,0,0,4,4,8,0 },
// ;
{ 2,4,8,16,8,4,2,0,0 },
// <
{ 0,0,31,0,31,0,0,0,0 },
// =
{ 8,4,2,1,2,4,8,0,0 },
// >
{ 14,17,2,4,4,0,4,0,0 },
// ?
{ 14,17,23,21,23,16,14,0,0 }, // @
{ 4,10,17,17,31,17,17,0,0 }, // A
{ 30,17,17,30,17,17,30,0,0 }, // B
{ 14,17,16,16,16,17,14,0,0 }, // C
{ 30,17,17,17,17,17,30,0,0 }, // D
{ 31,16,16,30,16,16,31,0,0 }, // E
{ 31,16,16,30,16,16,16,0,0 }, // F
{ 14,17,16,16,19,17,15,0,0 }, // G
{ 17,17,17,31,17,17,17,0,0 }, // H
{ 14,4,4,4,4,4,14,0,0 },
// I
{ 1,1,1,1,1,17,14,0,0 },
// J
{ 17,18,20,24,20,18,17,0,0 }, // K
{ 16,16,16,16,16,16,31,0,0 }, // L
{ 17,27,21,21,17,17,17,0,0 }, // M
{ 17,17,25,21,19,17,17,0,0 }, // N
{ 14,17,17,17,17,17,14,0,0 }, // O
{ 30,17,17,30,16,16,16,0,0 }, // P
{ 14,17,17,17,21,18,13,0,0 }, // Q
{ 30,17,17,30,20,18,17,0,0 }, // R
{ 14,17,16,14,1,17,14,0,0 }, // S
{ 31,4,4,4,4,4,4,0,0 },
// T
{ 17,17,17,17,17,17,14,0,0 }, // U
{ 17,17,17,10,10,4,4,0,0 },
// V
{ 17,17,17,21,21,21,10,0,0 }, // W
102
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
};
17,17,10,4,10,17,17,0,0 },
17,17,10,4,4,4,4,0,0 },
31,1,2,4,8,16,31,0,0 },
0,4,8,31,8,4,0,0,0 },
16,16,16,16,22,1,2,4,7 },
0,4,2,31,2,4,0,0,0 },
0,4,14,21,4,4,0,0,0 },
10,10,31,10,31,10,10,0,0 },
0,0,0,31,0,0,0,0,0 },
0,0,14,1,15,17,15,0,0 },
16,16,30,17,17,17,30,0,0 },
0,0,15,16,16,16,15,0,0 },
1,1,15,17,17,17,15,0,0 },
0,0,14,17,31,16,14,0,0 },
2,4,4,14,4,4,4,0,0 },
0,0,15,17,17,17,15,1,14 },
16,16,30,17,17,17,17,0,0 },
4,0,12,4,4,4,14,0,0 },
4,0,4,4,4,4,4,4,8 },
8,8,9,10,12,10,9,0,0 },
12,4,4,4,4,4,14,0,0 },
0,0,26,21,21,21,21,0,0 },
0,0,30,17,17,17,17,0,0 },
0,0,14,17,17,17,14,0,0 },
0,0,30,17,17,17,30,16,16 },
0,0,15,17,17,17,15,1,1 },
0,0,11,12,8,8,8,0,0 },
0,0,15,16,14,1,30,0,0 },
4,4,14,4,4,4,2,0,0 },
0,0,17,17,17,17,15,0,0 },
0,0,17,17,10,10,4,0,0 },
0,0,17,17,21,21,10,0,0 },
0,0,17,10,4,10,17,0,0 },
0,0,17,17,17,17,15,1,14 },
0,0,31,2,4,8,31,0,0 },
8,8,8,8,9,3,5,7,1 },
10,10,10,10,10,10,10,0,0 },
24,4,24,4,25,3,5,7,1 },
0,4,0,31,0,4,0,0,0 },
31,31,31,31,31,31,31,0,0 }
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
X
Y
Z
left arrow
[ in ASCII
1/2
\ in ASCII
right arrow
] in ASCII
up arrow ^ in ASCII
#
_ in ASCII
dash
` in ASCII
a
b
c
d
e
f
g
h
i
j
k
l
m
n
o
p
q
r
s
t
u
v
w
x
y
z
1/4
{ in ASCII
||
| in ASCII
3/4
} in ASCII
divide sign
~ in ASCII
character-sized block
not defined in ASCII
103
# led.h
#ifndef LED_H
#define LED_H
void
void
void
void
led_init(void);
led_invert(void);
output32(unsigned int num);
output(unsigned int num);
#endif
/* LED_H */
# led.c
#include "led.h"
#include "memory.h"
/* Addresses of ARM GPIO devices (with conversion to virtual addresses)
* See BCM2835 peripherals guide
*/
static volatile unsigned int *gpioGPFSEL1 = (unsigned int *)
mem_p2v(0x20200004);
static volatile unsigned int *gpioGPSET0 = (unsigned int *)
mem_p2v(0x2020001c);
static volatile unsigned int *gpioGPCLR0 = (unsigned int *)
mem_p2v(0x20200028);
static volatile unsigned int *gpioGPLEV0 = (unsigned int *)
mem_p2v(0x20200034);
static volatile unsigned int *gpioGPPUD = (unsigned int *)
mem_p2v(0x20200094);
static volatile unsigned int *gpioPUDCLK0 = (unsigned int *)
mem_p2v(0x20200098);
static volatile unsigned int *gpioPUDCLK1 = (unsigned int *)
mem_p2v(0x2020009c);
/* Short delay loop */
static void delay(void)
{
unsigned int timer=150;
while(timer--)
asm ("mov r0, r0");
/* No-op */
}
void led_init(void)
{
unsigned int var;
/* Each GPIO has 3 bits which determine its function
* GPIO 14 and 16 are in GPFSEL1
*/
/* Read current value of GPFSEL1 */
var = *gpioGPFSEL1;
/* GPIO 16 = 001 - output */
var&=~(7<<18);
var|=1<<18;
/* GPIO 14 = 000 - input */
var&=~(7<<12);
/* Write back updated value */
*gpioGPFSEL1 = var;
104
/* Set up pull-up on GPIO14 */
/* Enable pull-up control, then wait at least 150 cycles
* The delay loop actually waits longer than that
*/
*gpioGPPUD = 2;
delay();
/* Set the pull up/down clock for pin 14*/
*gpioPUDCLK0 = 1<<14;
*gpioPUDCLK1 = 0;
delay();
/* Disable pull-up control and reset the clock registers */
*gpioGPPUD = 0;
*gpioPUDCLK0 = 0;
*gpioPUDCLK1 = 0;
}
static unsigned int led_status = 0;
void led_invert(void)
{
led_status = !led_status;
if(led_status)
*gpioGPCLR0 = 1<<16;
else
*gpioGPSET0 = 1<<16;
/* on */
/* off */
}
/* Shortish delay loop */
static void shortdelay(void)
{
unsigned int timer = 3000000;
while(timer--)
asm("mov r0, r0");
/* No-op */
}
/* Massively long delay loop */
static void longdelay(void)
{
unsigned int timer = 10000000;
while(timer--)
asm("mov r0, r0");
/* No-op */
}
static void output_n(unsigned int num, unsigned int count)
{
longdelay();
longdelay();
longdelay();
longdelay();
/* Flash quickly to indicate start */
*gpioGPCLR0 = 1<<16;
shortdelay();
*gpioGPSET0 = 1<<16;
105
shortdelay();
*gpioGPCLR0 = 1<<16;
shortdelay();
*gpioGPSET0 = 1<<16;
longdelay();
longdelay();
while(count--)
{
*gpioGPCLR0 = 1<<16;
if(num & 1)
longdelay();
else
shortdelay();
*gpioGPSET0 = 1<<16;
longdelay();
num = num >> 1;
}
}
void output32(unsigned int num)
{
output_n(num, 32);
}
void output(unsigned int num)
{
output_n(num, 8);
}
106
# mailbox.h
#ifndef MAILBOX_H
#define MAILBOX_H
extern unsigned int readmailbox(unsigned int channel);
extern void writemailbox(unsigned int channel, unsigned int data);
#endif
/* MAILBOX_H */
# mailbox.c
/*
* Access system mailboxes
*/
#include "mailbox.h"
#include "barrier.h"
#include "memory.h"
/* Mailbox memory addresses */
static volatile unsigned int *MAILBOX0READ = (unsigned int *)
mem_p2v(0x2000b880);
static volatile unsigned int *MAILBOX0STATUS = (unsigned int *)
mem_p2v(0x2000b898);
static volatile unsigned int *MAILBOX0WRITE = (unsigned int *)
mem_p2v(0x2000b8a0);
/* Bit 31 set in status register if the write mailbox is full */
#define MAILBOX_FULL 0x80000000
/* Bit 30 set in status register if the read mailbox is empty */
#define MAILBOX_EMPTY 0x40000000
unsigned int readmailbox(unsigned int channel)
{
unsigned int count = 0;
unsigned int data;
/* Loop until something is received from channel
* If nothing recieved, it eventually give up and returns 0xffffffff
*/
while(1)
{
while (*MAILBOX0STATUS & MAILBOX_EMPTY)
{
/* Need to check if this is the right thing to do */
flushcache();
/* This is an arbritarily large number */
if(count++ >(1<<25))
{
return 0xffffffff;
}
}
/* Read the data
* Data memory barriers as we've switched peripheral
*/
dmb();
data = *MAILBOX0READ;
dmb();
107
if ((data & 15) == channel)
return data;
}
}
void writemailbox(unsigned int channel, unsigned int data)
{
/* Wait for mailbox to be not full */
while (*MAILBOX0STATUS & MAILBOX_FULL)
{
/* Need to check if this is the right thing to do */
flushcache();
}
dmb();
*MAILBOX0WRITE = (data | channel);
}
108
# Makefile
# These values should work for any Linux where the arm-linux-gnueabihf gcc
# suite is installed, including Raspbian
#
# If they don't, you'll need to manually add the paths for gcc, ld, as and
# objcopy
CC:=arm-linux-gnueabihf-gcc
LD:=$(shell $(CC) -print-prog-name=ld)
AS:=$(shell $(CC) -print-prog-name=as)
OBJCOPY:=$(shell $(CC) -print-prog-name=objcopy)
# Location of libgcc.a (contains ARM AEABI functions such as numeric
# division)
#
# The standard libgcc.a in some (cross-compiling) versions of gcc appears
to
# contain Thumb2 instructions, which don't exist on the ARM1176. This
causes
# undefined instruction exceptions. If the code is being compiled on a
# Raspberry Pi, it ought to be fine. Otherwise, you may need to use a
# different libgcc.a to the one which comes with your compiler
#
# If compiling on a Broadcom 2708 (a Raspberry Pi), use the libgcc.a
# provided by the compiler. Otherwise, use rpi-libgcc/libgcc.a (this won't
# definitely work on anything other than arm-linux-gnueabihf-gcc 4.6, but
# is likely to)
# If LOCALLIBGCC is set, use the libgcc.a supplied with the compiler
ifdef LOCALLIBGCC
LIBGCC:=$(shell $(CC) -print-libgcc-file-name)
endif
# If this is a Raspberry Pi (specifically, a Broadcom 2708 SoC), use the
# libgcc.a supplied with the compiler, unless an alternative is set in
# $(LIBGCC)
ifneq ($(shell grep BCM2708 /proc/cpuinfo 2>/dev/null),)
LIBGCC ?= $(shell $(CC) -print-libgcc-file-name)
endif
# If no alternative is set in $(LIBGCC) by this point, use rpilibgcc/libgcc.a
LIBGCC ?= rpi-libgcc/libgcc.a
# Assembler options:
# -mcpu=arm1176jzf-s will cause the assembler to error on ARM opcodes which
are
# not valid for that specific processor
ASOPT=--warn -mcpu=arm1176jzf-s
# Compiler options:
# -marm prevents gcc building Thumb code
# -mcpu sets the exact CPU in the RPi, for optimised code
# -nostdinc so as not to use any system #include locations
# -ffreestanding to tell the compiler the usual system libraries aren't
# available
CCOPT=-Wall -O6 -nostdinc -ffreestanding -marm -g -mcpu=arm1176jzf-s
# Object files built from C
COBJS=framebuffer.o initsys.o interrupts.o led.o mailbox.o \
109
main.o memory.o memutils.o textutils.o scheduler.o process.o
syscall.o
# Object files build from assembler
ASOBJS=start.o
all: make.dep kernel.img
clean:
rm -f make.dep *.o kernel.elf kernel.img
.PHONY: all clean
# Build the list of dependencies included at the bottom
make.dep: *.c *.h
gcc -M $(COBJS:.o=.c) >make.dep
# If gcc -M fails, delete make.dep rather than allowing a half-finished
file
# to sit around for the next build
.DELETE_ON_ERROR: make.dep
# Build the assembler bits
start.o: start.s
$(ASOBJS):
$(AS) $(ASOPT) -o $@ $<
# Make an ELF kernel which loads at 0x8000 (RPi default) from all the
object
# files
kernel.elf: linkscript $(ASOBJS) $(COBJS)
$(LD) -T linkscript -nostdlib -nostartfiles -gc-sections \
-o kernel.elf \
$(ASOBJS) $(COBJS) $(LIBGCC)
# Turn the ELF kernel into a binary file. This could be combined with the
# step above, but it's easier to disassemble the ELF version to see why
# something's not working
kernel.img: kernel.elf
$(OBJCOPY) kernel.elf -O binary kernel.img
# Generic builder for C files
$(COBJS):
$(CC) $(CCOPT) -c -o $@ $<
# Include the auto-generated make.dep file. The hyphen before "include"
# stops it complaining that the file isn't there.
-include make.dep
110
# linkscript
/* Linker script for kernel */
/* Define the kernel entry point. Needed to prevent ld's -gc-sections
option
* getting rid of everything
*/
ENTRY(_start)
/* Define the memory as starting at 0x8000, and being a megabyte long
* (shouldn't need to be more than this)
* "kernel" is where the majority of the kernel is run - it is linked at
* this address, but loaded into memory after initsys at 0x9000 (probably,
* as long as initsys is less than 4K)
*/
MEMORY
{
initsys : org = 0x8000, len = 1M
kernel : org = 0xf0000000, len = 1M
data : org = 0xc0000000, len = 1M
}
/* Output kernel code (.text) and read-only data (.rodata) into kernel
* code space (0xf0000000), read/write data (.data) and initialised-to-zero
* data (.bss) into kernel data space (0xc0000000), and the initial
* start-up code/data (in start.s/initsys.c) at 0x8000
*
* Make sure start.o comes first, so the entry point is at the start of the
* file
*
* The file ends up looking something like this:
*
*
File offset Load address
Remapped to
*
0x00000000 0x00008000 0x00008000 start.o/initsys.o
*
0x00001000 0x00009000 0xf0009000 .text, .rodata
*
0x00010000 0x00018000 0xc0000000 .data
*
n/a
0x00020000 0xc0008000 .bss
*
* Various pointers are calculated to help initsys map the kernel's virtual
* memory, and clear .bss
*/
SECTIONS
{
/* Initial section, loads/runs at 0x8000 */
.init : {
start.o(.text* .data* .bss* .rodata*)
initsys.o (.text* .data* .bss* .rodata*)
} >initsys
_highkernelload = ALIGN(4k);
/* Main kernel text/data. Loads at 0x9000, is remapped to
* 0xf0009000 by initsys
*/
.text (_highkernelload + 0xf0000000) : AT(_highkernelload) {
_kstart = ABSOLUTE(.); /* Address of the kernel */
*(.text*)
} >kernel
.rodata : {
111
/* Where the kernel code ends and read-only data begins */
_krodata = ABSOLUTE(.);
*(.rodata*)
} >kernel
_kend = .; /* Address of the end of the kernel (RO data) */
_data_kmem = ALIGN(4k);
/* Kernel read/write data */
.data : AT(_data_kmem - 0xf0000000) {
_datastart = ABSOLUTE(.) ;
*(.data) }
>data
/* Kernel read/write memory - needs to be zeroed by the kernel before
* being used
*/
.bss : {
_bssstart = ABSOLUTE(.) ;
*(.bss)
*(COMMON)
_bssend = ABSOLUTE(.) ;
} >data
/* Calculate physical addresses of data memory for the kernel */
_physdatastart = _data_kmem - 0xf0000000;
_physbssstart = _physdatastart + (_bssstart - _datastart);
_physbssend = _physdatastart + (_bssend - _datastart);
}
112
# start-debug.sh
#!/bin/bash
qemu-system-arm -kernel kernel.img -cpu arm1176 -M raspi -m 512 -s -vnc :1
Download

universidade do vale do itajaí centro de ciências