Sandro Emanuel Salgado Pinto. Sistema Operativo Orientado a Objetos: porting, expansão e configuração. Universidade do Minho Escola de Engenharia

Tamanho: px
Começar a partir da página:

Download "Sandro Emanuel Salgado Pinto. Sistema Operativo Orientado a Objetos: porting, expansão e configuração. Universidade do Minho Escola de Engenharia"

Transcrição

1 Universidade do Minho Escola de Engenharia Sandro Emanuel Salgado Pinto Sistema Operativo Orientado a Objetos: porting, expansão e configuração UMinho 2012 Sandro Emanuel Salgado Pinto Sistema Operativo Orientado a Objetos: porting, expansão e configuração Outubro de 2012

2 Universidade do Minho Escola de Engenharia Sandro Emanuel Salgado Pinto Sistema Operativo Orientado a Objetos: porting, expansão e configuração Tese de Mestrado Ciclo de Estudos Integrados Conducentes ao Grau de Mestre em Engenharia Eletrónica Industrial e Computadores Trabalho efetuado sob a orientação do Professor Doutor José Mendes Outubro de 2012

3 Agradecimentos As primeiras palavras de agradecimento são direcionadas aos meus pais, Manuel Pinto e Paula Salgado, por todo o apoio educacional, psicológico e financeiro, prestado durante todo o meu percurso académico, pois sem eles esta dissertação nunca seria uma realidade. Ao meu orientador Professor Doutor José Mendes, bem como aos Professores Doutores Adriano Tavares e Jorge Cabral, por todo o apoio prestado e por toda confiança depositada em mim para a concretização deste trabalho. Ao Embedded System Research Group do Departamento de Eletrónica Industrial da Universidade do Minho, que me acolheu e proporcionou todas as condições necessárias para a elaboração da dissertação. Um obrigado especial para o mestre Nuno Cardoso que sempre se mostrou disponível para me ajudar, esclarecer e partilhar conhecimentos. Aos meus colegas de curso que me acompanharam ao longo destes anos, em especial ao Tiago Castro e Vítor Veiga que estiveram envolvidos no projeto onde se integra a dissertação, bem como ao Filipe Alves por todos os momentos de companheirismo vividos. Finalmente, e não menos importante, à minha namorada, Bárbara Fernandes, e ao meu grupo de amigos, CN (Filtros, Fox, Maia, Marco, Milu, Moura, Peste, Rica, Rojão, Slim), por me terem alegrado, compreendido e apoiado sobretudo nos momentos de maior angústia e desilusão. A todos, um muito obrigado! iii

4

5 Resumo Nos últimos anos, cerca de 98% da produção anual de microprocessadores teve como finalidade os sistemas embebidos [1]. No entanto, o desenvolvimento de software e aplicações bare-metal pode tornar-se complexo, provocando uma enorme pressão no time-to-market, aumento do tempo e esforço (colaboradores/hora) de desenvolvimento, e deficiente qualidade do sistema final. A estratégia passa então por usar sistemas operativos, tornando o desenvolvimento mais simples, rápido e seguro. Normalmente, os sistemas operativos monolíticos não se adequam às necessidades e limitações dos sistemas embebidos, pois maximizam o número de plataformas e funcionalidades oferecidas, o que se traduz num aumento no consumo de recursos. Por isso, a tendência recai sobre sistemas operativos de tempo-real (baseados em microkernel) desenvolvidos e adaptados à arquitetura do processador, e aos requisitos e restrições da aplicação. No entanto, com o aumento da complexidade dos sistemas atuais, existe uma procura crescente na configurabilidade, variabilidade e reutilização dos sistemas embebidos. A maioria desses sistemas gere a variabilidade utilizando compilação condicional ou programação orientado a objetos. A primeira aumenta a complexidade de gestão do código. A última providencia a modularidade e adaptabilidade necessários para simplificar a tarefa de desenvolvimento de software reutilizável e customizável, no entanto, degrada o desempenho e os recursos de memória do sistema. Neste sentido, a presente dissertação propõe a utilização de C++ template metaprogramming como a metodologia para a gestão da variabilidade de um sistema operativo orientado a objetos. Utilizando esta técnica de programação, é possível gerar apenas as funcionalidades pretendidas, garantindo assim código otimizado e ajustado às necessidades da aplicação e aos recursos de hardware. v

6

7 Abstract In recent years, approximately 98% of microprocessors annual production was aimed at embedded systems [1]. However, the development of bare-metal application software can become complex, leading to a tremendous pressure on time-to-market, increased time and effort development (staff / hour), and poor final system quality. So, the strategy is to use operating systems, making development easier, faster and safer. Typically, monolithic operating systems do not fit the requirements and limitations of embedded systems since they attempt to maximize the number of supported platforms and functionalities offered, which results in an increase in the consumption of resources. Therefore, the trend became using real-time operating systems (microkernel based) developed and adapted to processor architecture and to application requirements and constraints. However, with the growing complexity of current systems, there is an increasing demand for configurability, variability and reuse of embedded systems. Most of these systems manage variability using conditional compilation or object oriented programming. The former paradigm increases the management complexity of code. The latter provides the modularity and adaptability needed to simplify the task of developing reusable and customizable software; however, it degrades performance and memory resources. In this context, this thesis proposes the use of C++ template metaprogramming as a methodology for managing the variability of an object-oriented operating system. Using this advanced programming technique, it is possible to generate only the desired functionalities, thus ensuring that code is optimized and adjusted to application requirements and hardware resources. vii

8

9 Conteúdo 1 Introdução Contextualização Objetivos Organização da Dissertação Estado da Arte Programação Orientada a Objetos Paradigmas de Programação Objetos e Classes Princípios Fundamentais Sistemas Operativos Arquitetura dos Sistemas Operativos Sistemas Operativos de Tempo-Real Sistemas Operativos Orientados a Objetos Configurabilidade e Variabilidade no Software: técnicas de programação Compilação Condicional Orientação a Objetos Orientação a Componentes Orientação a Funcionalidades Orientação a Aspetos Programação Generativa Conclusões Especificação do Sistema Microcontrolador Arquitetura de Memória ix

10 3.1.2 Registos Básicos Periféricos Interrupções Arquitetura do Conjunto de Instruções ADEOS: A Decent Embedded Operating System Tarefas Escalonador Sincronização de Tarefas Template MetaProgramming Blocos Básicos do Template Metaprogramming O Fatorial Lista Ligada Estática Ambiente de Desenvolvimento Compilador IAR C/C++ para o Implementação do Sistema Porting do ADEOS para a Plataforma MCS Análise do Código Dependente do Processador Porting do Código Dependente do Processador Upgrade do ADEOS Upgrade: clock-tick no escalonador Upgrade: device drivers Upgrade: escalonador power-aware Refactoring do ADEOS Diagrama de Funcionalidades Estratégia de Gestão da Variabilidade Reestruturação do ADEOS Resultados Experimentais Ambiente de Testes Métricas de Teste Testes Realizados Teste ao Sistema Operativo Teste ao driver USART x

11 6 Conclusões Conclusão Trabalho Futuro A Placa Circuito Impresso: spi2c 145 xi

12

13 Lista de Figuras 2.1 Modelos arquiteturais de um sistema operativo: monolítico e microkernel Diagrama de classes do core do sistema operativo BOSS Orientação a funcionalidades: hierarquia de classes e modelo de funcionalidades Junção do código aspeto no código dos componentes Diagrama de blocos do microcontrolador 8051 clássico Mapa de memória do 8051 genérico Diagrama de classes do ADEOS Relação dos estados das tarefas no ADEOS Ilustração da lista de tarefas prontas a executar (readylist) Resolução dos templates no cálculo do fatorial Processo de compilação de código fonte em código executável/máquina Arquitetura de software do ADEOS Pilha do sistema após entrada na função contextinit Pilha da tarefa após inicialização Diagrama de classes do driver PWM Diagrama de classes do driver UART Diagrama de classes do driver GPIO Formato da trama I 2 C Diagrama de classes do driver I 2 C SPI: barramento e diagrama temporal Diagrama de classes do driver SPI Diagrama de funcionalidades do ADEOS Placa de desenvolvimento 8051DKUSB xiii

14 5.2 Diagrama de funcionalidades do sistema operativo (teste ao sistema operativo) Resultados de desempenho e footprint de memória (teste ao sistema operativo) Resultados de gestão do código (teste ao sistema operativo) Resultados de desempenho e footprint de memória (teste ao driver USART) Resultados de gestão do código (teste ao driver USART) A.1 PCB spi2c: esquemático A.2 PCB spi2c: layout xiv

15 Lista de Tabelas 2.1 Implementação da classe Shape em C++ e Java Vetores de interrupção na família MCS Modos de endereçamento do Resultados de desempenho e memória das aplicações Fatorial (C++ dinâmico) e Fatorial (TMP) Código C++ TMP e código assembly da aplicação estática do fatorial Convenções de chamada de funções no compilador C/C da IAR Registos utilizados nos parâmetros das funções Registos utilizados no retorno das funções Rotina de interrupção do temporizador Configuração do temporizador Inicialização da contagem do temporizador Implementação C++ e assembly da seleção da frequência Classes especificas da funcionalidade example Declaração da classe template da funcionalidade Sched Definição das templates genérica e especificas da funcionalidade Sched Declaração da classe template da funcionalidade Task Definição das templates genérica e especificas da funcionalidade Task Características de hardware da placa de desenvolvimento 8051DKUSB Configuração usada no teste ao sistema operativo xv

16

17 Lista de Listagens 2.1 Implementação C (iterativa) do cálculo do fatorial de um número Implementação Haskell do cálculo do fatorial de um número Definição da classe Triangle como herdeira da classe Shape Exemplo de polimorfismo dinâmico na classe Triangle Declaração da classe Shape como abstrata Exemplo da utilização de compilação condicional Declaração da classe Task Função de iniciação das tarefas - run Construtor da classe Task Método schedule da classe Sched Construtor da classe Mutex Valores em template metaprogramming Funções em template metaprogramming Saltos condicionais em template metaprogramming Recursividade em template metaprogramming Implementação C++ recursiva do cálculo do fatorial Implementação C++ TMP recursiva do cálculo do fatorial Implementação C++ TMP de uma lista ligada estática de inteiros Metafunção Length da lista ligada estática Função de interrupção de overflow do timer Exemplo de utilização de inline assembler no compilador IAR Definição de uma função implementada num ficheiro assembly externo Estrutura de um ficheiro assembly gerado pelo compilador IAR Ficheiro bsp.h para a arquitetura Protótipo da função contextinit Protótipo da função contextswitch xvii

18 4.4 Definição da estrutura do estado da máquina (8051) de cada tarefa Macros para delimitação de uma secção crítica Macro para comutação de contexto (ContextSwitch) Configuração do clock-tick do escalonador Declaração da classe Pwm Estrutura de configuração da classe Pwm Enumerações da classe Pwm Método config da classe Pwm Declaração da classe Uart Construtor da classe Uart8051 com configuração Métodos txstart e rxstart da classe Uart Declaração da classe Gpio Método config da classe Gpio Declaração da classe I2c Construtor por defeito da classe I2c Métodos start e write char da classe I2c Declaração da classe Spi Construtor da classe Spi8051 com configuração Métodos read char da classe Spi Construtor da classe do escalonador power-aware Alterações na ISR do clock-tick do escalonador Implementação do método defer Ficheiro example tmp.h Transparência no código de acesso à funcionalidade example Transparência no código de acesso à funcionalidade Sched Transparência no código de acesso à funcionalidade Task xviii

19 Capítulo 1 Introdução Neste capítulo é contextualizado o âmbito desta dissertação, são definidos os objetivos a atingir, finalizando o capítulo com a organização da dissertação. 1.1 Contextualização Vivemos na era tecnológica. As sociedades modernas atuais estão cada dia mais dependentes de sistemas eletrónicos e informáticos responsáveis por substituir e simplificar as tarefas do quotidiano. Seja na indústria, na medicina, na aviação, ou simplesmente em casa, cada vez mais existe uma necessidade de desenvolver soluções computacionais que auxiliem a realização dessas tarefas. Neste sentido, acompanhando essa tendência, tem-se verificado um crescimento exponencial na utilização de microcontroladores no desenvolvimento e concepção de sistemas embebidos. Nos últimos anos, cerca de 98% da produção anual de microprocessadores teve como finalidade esse tipo de sistemas [1]. No entanto, o desenvolvimento de software e aplicações bare-metal pode tornar-se complexo, provocando uma enorme pressão no time-to-market, aumento do tempo e esforço (colaboradores/hora) de desenvolvimento, e deficiente qualidade do sistema final. Isto é extremamente desfavorável numa sociedade extremamente competitiva e capitalista como a atual, pois traduz-se num aumento dos custos, provável diminuição das vendas e consequente minimização dos lucros. Assim sendo, a estratégia passa por usar sistemas operativos, de forma a que o processo de desenvolvimento seja mais simples, rápido e seguro. Um sistema operativo é uma camada de software que abstrai o utilizador das especificidades do hardware, atuando como um intermediário entre o utilizador e os 1

20 1.1. Contextualização dispositivos [2]. Tem como principal objetivo, fornecer os recursos e meios ao utilizador para que este desenvolva e execute os programas de forma simplificada e eficiente. Normalmente, os sistemas operativos monolíticos não se adequam às necessidades e limitações do domínio embebido. Isto porque estes sistemas operativos procuram maximizar não só o número de plataformas alvo, mas também as funcionalidades oferecidas ao utilizador, o que se traduz num aumento no consumo de recursos (memória). Neste caso, a tendência recai sobre sistemas operativos de tempo real (baseados em microkernel) desenvolvidos e adaptados à arquitetura do processador, e aos requisitos e restrições da aplicação. No entanto, com o aumento da complexidade dos sistemas atuais, existe uma procura crescente na configurabilidade, variabilidade e reutilização dos sistemas embebidos. Um bom exemplo é o sistema operativo embebido especificado pela AUTOSAR [3]. A maioria desses sistemas gere a variabilidade utilizando compilação condicional. A implementação das funcionalidades configuráveis é embebida em blocos ifdef - endif, aumentando a complexidade na compreensão e manutenção do código. Uma outra abordagem consiste na utilização do paradigma orientado a objetos. Esta metodologia providencia a abstração, modularidade, e adaptabilidade necessários para simplificar a tarefa de desenvolvimento de software reutilizável, com funcionalidades variáveis e configuráveis. Apesar de todos os benefícios associados à programação orientada a objetos, características como a múltipla herança, o polimorfismo dinâmico, e a abstração diminuem o desempenho e introduzem um enorme overhead de memória [4, 5]. Este fator torna-se extremamente crítico sobretudo em sistemas com escassez de recursos como os sistemas embebidos. Daí que seja necessário utilizar técnicas de programação avançada que contornem e resolvam este problema. Programação generativa, nomeadamente template metaprogramming (TMP) [6, 7], é uma das técnicas que apresenta resultados muito promissores [8, 9, 10, 11, 12]. Com esta metodologia toda a variabilidade é processada pelo compilador durante a fase de resolução dos templates, ou seja, todo o processamento é realizado em tempo de compilação e não em tempo de execução. Desta forma, é possível gerar apenas as funcionalidades pretendidas, garantindo assim código otimizado e ajustado às necessidades da aplicação e dos recursos de hardware. A biblioteca Boost.MPL [13] é um exemplo de uma framework que usa C++ template metaprogramming para implementar algoritmos e metafunções de forma estática, isto é, no instante de compilação. 2

21 Capítulo 1. Introdução Assim sendo, o trabalho da presente dissertação pode ser dividido em duas partes: primeiro (i) avaliar os sistemas operativos orientados a objetos, de modo a efetuar o porting e expandir o sistema operativo que mais se adeque aos recursos disponibilizados pela arquitetura da família MCS-51; e posteriormente (ii) reestruturar o sistema operativo de modo a permitir a sua customização, gerindo a variabilidade do sistema sem porventura comprometer o seu desempenho e consumo de memória. Finalmente, e para terminar, importa referir que este trabalho é apenas uma fração de um projeto de maior dimensão que consiste também (i) no desenvolvimento de um microcontrolador da família MCS-51 low-power customizável, assim como (ii) um IDE capaz de gerar o sistema operativo e o microcontrolador, configurados de acordo com as necessidades e especificações do utilizador. Basicamente, o microcontrolador lowpower permite executar o sistema operativo, que é customizado e gerado de acordo com a configuração desejada pelo utilizador no IDE. 1.2 Objetivos O principal objetivo da presente dissertação está bastante claro no título da mesma. Assim, este passa por fazer o porting, o upgrade e a customização de um sistema operativo orientado a objetos para a arquitetura do microcontrolador MCS-51. Com base neste objetivo central, é possível ramificá-lo em vários objetivos parciais. Assim sendo, o primeiro passa por selecionar e efetuar o porting de um sistema operativo orientado a objetos para a plataforma MCS-51. Com base nos sistemas operativos orientados objetos disponíveis, é necessário perceber qual ou quais são mais propensos para os sistemas embebidos, e qual o que se torna a solução mais adequada para a finalidade da dissertação. O segundo objetivo consiste em melhorar e aumentar o conjunto de funcionalidades do sistema operativo escolhido. Otimizar o código dependente do processador, desenvolver um conjunto de device drivers para os vários periféricos do microcontrolador, e expandir as funcionalidades (escalonadores, IPCs, etc.) do sistema operativo, são algumas das tarefas necessárias à concretização deste objectivo. O terceiro objetivo consiste na aplicação de técnicas de programação avançadas para customizar, gerir a variabilidade e minimizar o overhead do sistema operativo orientado a objetos. Pretende-se, portanto, realizar o refactoring do sistema utilizando template metaprogramming. 3

22 1.3. Organização da Dissertação Finalmente, o quarto e último objectivo passa por avaliar quais os ganhos obtidos com a aplicação do template metaprogramming na gestão da variabilidade, desempenho e overhead de memória do sistema, em comparação com a implementação utilizando polimorfismo dinâmico. 1.3 Organização da Dissertação No capítulo 1 é feita uma pequena introdução onde é contextualizado o trabalho, são especificados os objetivos e é descrita a estrutura da presente dissertação. O capítulo 2 apresenta a fundamentação teórica dos conceitos abordados na dissertação, nomeadamente a programação orientada a objetos, os sistemas operativos, e as metodologias para a gestão da variabilidade do software. Além dos conceitos teóricos, é discutida e justificada a escolha do sistema operativo orientado a objetos, bem como a técnica de programação utilizada na gestão de variabilidade do sistema. No capítulo 3 são explicados, numa aproximação bottom-up, cada uma das camadas e componentes que compõe a base do sistema a implementar. Será analisada e explicada a arquitetura do microcontrolador 8051, o sistema operativo ADEOS, a técnica de template metaprogramming e compilador C++ da IAR para o O capítulo 4 descreve o desenvolvimento dos componentes do sistema. Basicamente, descreve o trabalho propriamente desenvolvido, nomeadamente o porting do sistema operativo ADEOS para a arquitetura MCS-51, as melhorias introduzidas e, no final, a reestruturação do sistema com template metaprogramming, de modo a gerir a variabilidade e permitir a sua customização. No capítulo 5 são apresentados os resultados experimentais dos testes realizados. Para avaliar as métricas de desempenho e gestão do código, foram efetuados dois testes distintos. No primeiro, o sistema operativo e as diversas funcionalidades foram implementadas utilizado template metaprogramming e polimorfismo dinâmico. No segundo, apenas foi testado um módulo driver, isto para comparar os resultados com uma implementação na linguagem de programação mais utilizada em sistemas embebidos (C). O documento termina com o capítulo 6, que traduz as principais conclusões do trabalho realizado, assim como enuncia algumas sugestões para trabalho futuro, que visam melhorar e expandir o trabalho desenvolvido. 4

23 Capítulo 2 Estado da Arte Este capítulo apresenta uma visão geral dos principais conceitos abordados nesta dissertação. Sendo o objeto de estudo desta dissertação a customização e gestão da variabilidade de um sistema operativo orientado objetos, torna-se portanto essencial clarificar e explicar os conceitos fundamentais sobre as três principais temáticas adjacentes ao trabalho a desenvolver: (i) a programação orientada a objetos; (ii) os sistemas operativos; e (iii) as diferentes metodologias de programação para a gestão da variabilidade do software. Para além de todo o fundamento teórico, são também apresentadas as decisões relativamente ao sistema operativo orientado objetos, bem como a técnica de gestão de variabilidade, a adoptar para concretizar o trabalho proposto. 2.1 Programação Orientada a Objetos A Programação Orientada a Objetos (POO) surgiu com a necessidade de tornar as aplicações de software mais próximas do modelo usado pelas pessoas para pensar e lidar com o mundo. Em metodologias e paradigmas de programação mais antigos, sempre que um programador se depara com um problema, a sua preocupação consiste em identificar uma tarefa de computação responsável por solucionar esse problema. Assim sendo, a programação consiste apenas em encontrar uma sequência de instruções capaz de realizar a tarefa pretendida. No entanto, o conceito de programação orientada a objetos é totalmente diferente. Em vez de tarefas, existem objetos, ou seja, entidades que têm comportamentos, retêm informação, e que podem interagir com outros objetos. Segundo este paradigma, programar consiste em desenhar um 5

24 2.1. Programação Orientada a Objetos conjunto de objetos responsável por modelar e resolver o problema pretendido. Estes objetos podem representar entidades reais ou abstratas no domínio do problema. Desta forma, é suposto tornar o processo de desenvolvimento mais simples e natural, e por isso, mais fácil de entender. Resumindo, a programação orientada a objetos fornece um conjunto de ferramentas e métodos que possibilitam aos programadores desenvolver software confiável, sustentável, reutilizável e bem documentado, e que simultaneamente cumpre os requisitos pretendidos pelos utilizadores Paradigmas de Programação Além do paradigma orientado a objetos, existem outros paradigmas de programação: (i) a programação imperativa (por exemplo, utilizado em linguagens como C[14], Basic[15] e Pascal[16]); (ii) a programação lógica (por exemplo, Prolog[17]); e (iii) a programação funcional (por exemplo, Haskell[18] ou Lisp[19]). As linguagens de programação imperativas concebem um programa como um conjunto de funções e sub-rotinas que realizam uma determinada tarefa. Por exemplo, o código escrito em C da listagem 2.1 pode ser utilizado para calcular, iterativamente, o fatorial de um número. int fatorial (int num) int result = 1; for (int count = 1; count <= num; count++) result = count; return result; } Listagem 2.1: Implementação C (iterativa) do cálculo do fatorial de um número A programação funcional é um paradigma de programação que trata a computação como uma avaliação de funções matemáticas, o que evita estados ou dados mutáveis. Este tipo de paradigma enfatiza a definição de funções, em contraste com a programação imperativa, que enfatiza a execução de comandos sequenciais. O código apresentado na listagem 2.2 calcula o fatorial de um número em Haskell. factorial :: Int > Int factorial 0 = 1 factorial n = n factorial (n 1) Listagem 2.2: Implementação Haskell do cálculo do fatorial de um número 6

25 Capítulo 2. Estado da Arte Prolog (PROgramming in LOGic) é a linguagem de programação mais usada segundo o paradigma da programação lógica. Este paradigma é baseado em ideais matemáticos de relações e inferência lógica. Isto significa que, mais do que descrever como computorizar uma solução, um programa consiste numa base de dados de regras lógicas que descrevem as relações para uma determinada aplicação. Por outras palavras, quando se executa um programa para obter uma solução, o utilizador responde a uma questão, e com base nessa resposta, o sistema procura (em tempo de execução), na base de dados, as regras que determinam (através de dedução lógica) a resposta pretendida. Resumindo: em linguagens imperativas, utilizam-se procedimentos; em linguagens funcionais, utilizam-se funções; em linguagens de programação lógicas, utilizam-se expressões lógicas; em linguagens orientadas a objetos, utilizam-se objetos Objetos e Classes Na programação orientada a objetos, tal como designação sugere, são criados objetos de software que modelam e representam os objetos do mundo real. Assim, tal como os objetos reais, os objetos de software são caracterizados por um determinado estado e comportamento. Esse estado é conservado nos atributos. Cada atributo é denominado por um identificador e é responsável por armazenar a informação e dados desse estado. Por sua vez, o comportamento do objeto é implementado através de métodos. Um método é então uma função associada a um determinado objeto. Portanto, um objeto é um componente de software que contém variáveis e métodos intrínsecos. Além disso, muitas vezes um objeto é designado por instância, uma vez que uma instância refere-se a um objeto em particular. Por exemplo, um Porsche Panamera é uma instância de um carro, pois refere-se a um carro em particular. Objetos e classes estão intrinsecamente relacionados. As classes são as entidades usadas para produzir e criar os objetos. Assim sendo, uma classe declara as variáveis necessárias para reter o estado de cada objeto, assim como fornece as implementações dos métodos necessários para operar sobre o estado do objecto. Portanto, só depois de ser criada a classe é que é possível criar ou instanciar objetos dessa classe. Por outras palavras, uma classe é uma espécie de planta para construir objetos. As partes 7

26 2.1. Programação Orientada a Objetos Tabela 2.1: Implementação da classe Shape em C++ e Java Linguagem C++ Java Código Exemplo class Shape public: Shape(int h, int w) h = h; w = w; } void seth(int h) h = h; } int geth() return h; } void setw(int w) w = w; } int getw() return w; } }; private: int h, w; class Shape public Shape(int h, int w) h = h; w = w; } public void seth(int h) h = h; } public int geth() return h; } public void setw(int w) w = w; } public int getw() return w; } }; private int h; private int w; não-estáticas da classe especificam ou descrevem que variáveis e métodos os objetos irão ter. Isto permite então estabelecer a distinção entre os dois conceitos: os objetos são criados e destruídos ao executar o programa, podendo ter a mesma estrutura, desde que sejam criados usando a mesma classe. A tabela 2.1 ilustra a definição de uma classe nas duas linguagens de programação orientadas a objetos mais utilizadas: C++ [20] e Java [21]. A classe exemplo representa um objeto do mundo concreto, nomeadamente, uma figura geométrica (Shape) Princípios Fundamentais Os princípios fundamentais de qualquer linguagem orientada a objetos são: (i) encapsulamento; (ii) herança; (iii) polimorfismo; e (iv) abstração. Encapsulamento O encapsulamento é uma caracteristica da POO que consiste em proteger as variáveis dos objetos através dos seus métodos. Basicamente, definindo-se os atributos como privados e os métodos como públicos, garante-se assim que os valores dos atributos apenas poderão ser modificados pelas regras que definem os métodos. Isto 8

27 Capítulo 2. Estado da Arte proporciona então grandes vantagens ao desenvolvimento de software sobretudo em dois aspectos: Modularidade: o código fonte de um objeto pode ser escrito e gerido independetemente do código fonte de outros objetos. Além disso, um objeto pode ser facilmente passado no sistema. Ocultação da Informação: um objeto tem uma interface pública que os outros objetos podem usar para comunicar com este. Assim, o objeto contém a informaçao privada e métodos que podem ser modificados a qualquer momento sem que os outros objetos dependam disso. O código da tabela 2.1 é um exemplo da utilização do encapsulamento em diferentes linguagens de programação. A classe Shape é constituída por dois atributos: w e h. O atributo w define o valor da largura (width) da figura, e o atributo h define o valor da altura (height). Os atributos da classe são definidos como privados (private), para que não seja possível a qualquer objeto externo aceder diretamente a cada uma das variáveis. Daí que sejam definidos os métodos seth, geth, setw e getw, para ler e escrever os valores de cada uma das variáveis. Os métodos são definidos como públicos (public), de forma a que sejam acessíveis a entidades externas à classe. Herança Na POO, a herança é uma metodologia de reutilização de software usado sempre que uma classe herda a estrutura e o comportamento de outra classe. Por outras palavras, através do mecanismo de subclasse, é possível herdar atributos e comportamentos (métodos) comuns da classe base (também designada superclasse ou classe pai), e acrescentar as especificidades a cada uma das subclasses. Portanto, pode-se dizer que a herança permite a customização e o refinamento incremental, isto é, cada subclasse para além dos métodos e atributos comuns pode ter métodos e atributos específicos. Seguindo o exemplo da figura geométrica, o código da listagem 2.3 define a classe Triangle, que implementa uma figura geométrica triângulo. class Shape... protected: int h, w; 9

28 2.1. Programação Orientada a Objetos }; class Triangle : public Shape public: Triangle(int h, int w) : Shape(h,w) } double area() return ( h w)/2; } }; Listagem 2.3: Definição da classe Triangle como herdeira da classe Shape Como a classe Triangle herda da classe Shape, todas as propriedades da classe Shape são herdadas pela classe Triangle. A classe Triangle define um novo método (area), que implementa o cálculo da área de um triângulo. Os atributos w e h são definidos pela classe base Shape, bem como os métodos públicos que permitem aceder a esses atributos. No exemplo apresentado, a subclasse herda apenas de uma classe base, no entanto é possível herdar de várias classes base (múltipla herança). Polimorfismo A palavra polimorfismo vem do grego e significa pode tomar várias formas. Assim, enquanto que a herança se refere às classes (e respectiva hierarquia), o polimorfismo diz respeito aos métodos dos objetos. Existem essencialmente três tipos de polimorfismo: (i) o polimorfismo ad-hoc, (ii) o polimorfismo paramétrico e (iii) o polimorfismo de herança ou dinâmico. O polimorfismo ad-hoc permite então ter funções com mesmo nome, com funcionalidades semelhantes, em classes sem nenhuma relação entre elas. O polimorfismo paramétrico representa a possibilidade de definir várias funções do mesmo nome mas possuindo parâmetros diferentes (em número e/ou tipo). O polimorfismo dinâmico permite redefinir um método (overwriting) em classes que são herdeiras de uma classe base, isto é, é possível fazer a especialização desse método. A listagem 2.4 apresenta um exemplo de polimorfismo dinâmico onde é definida classe Triangle que reimplementa a função virtual definida na classe Shape. class Shape public:... virtual double area() }... }; class Triangle : public Shape 10

29 Capítulo 2. Estado da Arte }; public:... double area() return ( h w)/2; } Listagem 2.4: Exemplo de polimorfismo dinâmico na classe Triangle A classe Triangle ao herdar da classe Shape reimplementa o método area. O método é reimplementado pela subclasse porque a classe base define o método como virtual. Assim, se o objeto criado for do tipo Shape, é chamado o método area da classe Shape. Caso contrário, se for criado um objeto do tipo Triangle, é chamado o método area da classe Triangle. Abstração A abstração é mais uma das características fundamentais da POO. Consiste numa forma de generalização que permite gerir melhor a complexidade. Assim sendo, significa que devemos considerar as qualidades e comportamentos independentemente dos objetos a que pertencem, e daí isolarmos os atributos que um determinado grupo de objetos tem em comum. Em C++, uma classe é considerada abstrata desde que contenha pelo menos um método virtual puro (pure virtual). Um método é considerado virtual puro quando é um método virtual igualado a zero, ou seja, é um método que pode ser reescrito na subclasse (com a mesma assinatura) igualado a zero. O código da listagem 2.5 é semelhante ao do exemplo anterior (listagem 2.4), no entanto a classe Shape é declarada como abstrata, uma vez que o método area é declarado como virtual puro (listagem 2.5). Neste exemplo, pode-se dizer que a classe Shape isola as características de um triângulo, como de tantas outras figuras geométricas. A classe Shape é então utilizada pela subclasse Triangle para herdar os seus atributos e métodos, no entanto os últimos devem ser implementados especificamente. Por outras palavras, a classe abstrata Shape serve como base para outras classes que queiram ser do mesmo grupo de objetos (Triangle). Por isso, a classe Shape não pode ser instanciada, daí que todos os métodos declarados como abstratos deveram ser implementados pelas subclasses (Triangle). class Shape public:... 11

30 2.2. Sistemas Operativos }; virtual double area() = 0;//pure virtual... Listagem 2.5: Declaração da classe Shape como abstrata 2.2 Sistemas Operativos A secção anterior terminou com a definição do conceito de abstração no âmbito da programação orientada a objetos. Um sistema operativo é também uma abstração, pois fornece uma camada de software que abstrai o utilizador das especificidades do hardware subjacente. Basicamente, este atua como um intermediário entre o utilizador e o hardware do computador [22]. Assim sendo, o objectivo de um sistema operativo passa por fornecer recursos e meios ao utilizador para que este execute os programas de forma simplificada e eficiente. Basicamente, este é responsável por controlar cada ficheiro, cada dispositivo, cada endereço de memória, e cada unidade de tempo de processamento Arquitetura dos Sistemas Operativos O kernel constitui o núcleo de um sistema operativo. Este representa a parte mais importante, e indispensável, do sistema. Basicamente, um sistema operativo está dividido em duas partes: o espaço do kernel (modo privilegiado) e o espaço do utilizador (modo sem privilégios). Sem isso, a proteção entre os processos seria impossível. Existem essencialmente dois modelos arquiteturais (conceitos de kernel) que permitem caracterizar os sistemas operativos: (i) monolíticos; e (ii) microkernel. A primeira arquitetura, monolítica, executa cada um dos serviços básicos, como o gestor de tarefas, gestor de memória, gestor de interrupções, gestor de dispositivos, sistema de ficheiros, etc., em modo kernel (figura 2.1). Este modelo encontra-se organizado em camadas, construídas a partir do gestor de tarefas (modo privilegiado) até às interfaces com o resto do sistema operativo - bibliotecas e por cima delas as aplicações (modo sem privilégios). A inclusão de todos os serviços básicos no espaço do kernel tem três grandes inconvenientes: o tamanho do kernel, a falta de extensibilidade e a dificuldade de manutenção. Sempre que se pretender corrigir um bug ou a adicionar um novo recurso, é necessário recompilar o kernel todo. Esta 12

31 Capítulo 2. Estado da Arte operação consome muito tempo e recursos, pois a compilação pode levar várias horas e consumir avultadas quantidades de memória. Figura 2.1: Modelos arquiteturais de um sistema operativo: monolítico e microkernel Para superar as limitações de extensibilidade e facilidade de manutenção, surgiu o modelo baseado em microkernel. A estratégia (figura 2.1) consistiu na redução dos serviços implementados no espaço do kernel. Apenas serviços básicos de comunicação entre processos, escalonador e gestor de memória virtual foram implementados em modo privilegiado. Os outros serviços do sistema (sistema de ficheiros, device drivers, pager) residem no espaço do utilizador em forma de processos normais (como servidores de chamadas). Como os servidores deixam de ser executados no espaço do kernel, então é necessário implementar as chamadas trocas de contexto, para permitir aos processos de utilizador entrar e sair em modo privilegiado. Como a comunicação deixa de ser feita de forma direta, foi necessário introduzir um sistema de mensagens que permite a comunicação independente e favorece a extensibilidade. Sistema Operativo monolítico: GNU/Linux O sistema operativo GNU/Linux [23] é uma implementação open source do sistema Unix, desenvolvido por milhares de pessoas. Este sistema representa uma implementação típica de um kernel monolítico. Todas as funções do sistema, incluindo gestor de tarefas, gestor de memória, escalonador, funcionalidades I/O e drivers são 13

32 2.2. Sistemas Operativos implementados no espaço do kernel. O tamanho estimado do kernel monolítico deste sistema é de algumas dezenas de megabytes, o que resulta num processo de manutenção bastante complexo e fatigante. Sistema Operativo baseado em microkernel: QNX O QNX (Quick Unix) [24] é uma das implementações mais populares de um sistema operativo baseado em microkernel desenvolvido para aplicações em tempo real. Apenas os serviços mais básicos, como escalonador, temporizadores e signals residem dentro do espaço do kernel, o que resulta num tamanho do kernel de 64k-byte. Todos os outros componentes, por exemplo, pilhas de protocolos, drivers e sistema de ficheiros, são executado no espaço do utilizador. O kernel do QNX (designado neutrino) é implementado em C e, portanto, pode ser facilmente adaptado para diferentes plataformas Sistemas Operativos de Tempo-Real Um sistema operativo de tempo-real (RTOS) é concebido para atender as necessidades de aplicações de tempo-real. Estes sistemas são caracterizados pelo tempo que demoram a completar uma determinada tarefa. A finalidade neste tipo de sistemas não é o throughput, mas sim a garantia de cumprimento das deadlines. Num sistema em que o incumprimento ocasional numa deadline seja aceitável e não cause qualquer dano permanente ao sistema, é designado como soft real-time, no entanto caso seja necessário garantir determinismo e satisfação de todos os deadlines, este é designado como hard real-time. Sistemas de áudio e sistemas multimédia enquadram-se na primeira designação. Sistemas para controlo de processos industriais, de aviação e militares enquadram-se na segunda categoria. Com efeito, nos sistemas operativos de tempo-real é mais valorizado a rapidez e a previsibilidade da resposta do sistema, do que propriamente a quantidade de tarefas realizadas num determinado período de tempo. Portanto, a minimização da latência de interrupção e da latência de comutação entre tarefas, são aspectos preponderantes na concepção deste tipo de sistemas operativos. Daí que os algoritmos de escalonamento dos RTOS sejam um pouco complexos. Alguns exemplos comuns são: rate-monotonic (RM), earliest deadline first (EDF) e highest priority first (HPF). Os sistemas operativos de tempo-real e a sua aplicabilidade em sistemas embebidos 14

33 Capítulo 2. Estado da Arte estão intrinsecamente relacionados. A grande maioria destes sistemas operativos são desenvolvidos para o domínio embebido. Isto porque geralmente os RTOS são baseados em microkernel, o que vai de encontro à escassez de recursos dos sistemas embebidos. Além disso, os sistemas embebidos são sistemas desenvolvidos com um propósito específico, para realizar um conjunto restrito e dedicado de tarefas com deadlines concretas [25]. Resumindo, existe uma correlação estrita entre os sistemas operativos de tempo-real e a sua aplicação no domínio embebido. Alguns exemplos de sistemas operativos de tempo-real para o domínio embebido são o LynxOS [26], o FreeRTOS [27] e QNX (referido anteriormente) Sistemas Operativos Orientados a Objetos Um sistema operativo orientado a objetos distingue-se dos sistemas operativos tradicionais (implementados com linguagens de programação imperativas) essencialmente por duas características fundamentais. Primeiro, porque o sistema operativo orientado a objetos deve ser desenhado e implementado segundo o paradigma orientado a objetos. Isto significa que todo o sistema operativo deve ser implementado através de um conjunto de objetos, que representam uma instância de cada uma das classes que o constituem. Além disso, os princípios fundamentais da programação orientada objetos (encapsulamento, herança, polimorfismo) devem ser utilizados para organizar as classes e as respectivas inter-relações. Principalmente, a herança e o polimorfismo paramétrico devem ser usados para facilitar a partilha e reutilização do código, assim como a configuração do sistema operativo. Um sistema operativo disponibiliza um conjunto de interfaces/primitivas que permitem às aplicações invocar funções do sistema (system calls) que implementam os serviços pretendidos. Portanto, um sistema operativo orientado a objetos distingue-se dos demais pois disponibiliza os seus serviços ou primitivas através de mensagens trocadas entre objetos. Por outras palavras, num sistema operativo orientado a objetos todas as entidades são representadas por objetos que são instâncias das respectivas classes. Alguns exemplos de classes a utilizar na implementação do sistema operativo podem ser: Processor, para representar fisicamente o processador; Scheduler, para representar o escalonador; Task, para representar uma tarefa de execução; e DeviceDriver, para representar um periférico. 15

34 2.2. Sistemas Operativos Vantagens dos sistemas operativos orientados a objetos Tal como a utilização do paradigma da orientação a objetos tem enumeras vantagens no desenho e concepção de aplicações de software, também os sistemas operativos beneficiam da utilização deste paradigma. Assim, desenhando e concebendo um sistema operativo orientado a objetos é possível obter vantagens sobretudo ao nível da portabilidade, reutilização de código, e gestão da complexidade e manutenção do código [28]. Quando se pretende desenvolver um sistema operativo portável, é essencial isolar as especificidades de determinados dispositivos em módulos separados das partes do sistema que são independentes da arquitetura (architecture independent modules). Para isso é necessário disponibilizar interfaces dos módulos dependentes da arquitetura (architecture dependent modules) para permitir aos projetistas desenhar e implementar o resto do sistema operativo sem a necessidade de saber os detalhes de implementação desses módulos. Desta forma, é possível reimplementar os módulos específicos sempre que se pretenda alterar a arquitetura, sem contudo modificar o resto do sistema operativo. A programação orientada a objetos permite implementar a portabilidade através das classes abstratas. Assim, estas podem ser usadas para definir as interfaces enquanto as classes concretas implementam as especificidades dos módulos dependentes das arquiteturas. Isto significa que criando as classes abstratas para definir as interfaces das entidades dependentes da arquitetura é possível desenvolver os algoritmos e a estrutura do sistema operativo sem conhecimento detalhado do hardware a ser usado. Outra das vantagens obtidas com a utilização do paradigma da orientação a objetos nos sistemas operativos é a reutilização de código. Geralmente, dispositivos semelhantes tem características comuns e por isso implementações semelhantes. Através do conceito de herança e classe abstrata é possível desenhar um sistema operativo reduzindo o código escrito e consequentemente aumentado a produtividade. Ou seja, as características comuns de um recurso podem ser abstraídas numa nova classe e as diferenças representadas em cada uma das classes derivadas ou subclasses. Exemplificando, imaginemos uma classe chamada DeviceDrivers e duas subclasses dessa classe base designadas Timer1 e Timer2 que têm características comuns, mas cujo código é repetido em cada uma das classes. Assim definindo uma classe abstrata designada por Timer que contém tudo que é comum e derivando duas subclasses dessa classe abstrata, é possível partilhar o código comum dos dispositivos. Além 16

35 Capítulo 2. Estado da Arte disso, ainda existe o benefício de que efetuando qualquer modificação na superclasse abstrata, por exemplo, a correção de um bug ou uma melhoria no desempenho de uma implementação, será automaticamente herdada pelas subclasses concretas. Combinando a herança e o polimorfismo é possível obter no sistema operativo benefícios ao nível da optimização da gestão de complexidade e da manutenção do código. Para entender como é possível optimizar recorrendo a estes conceitos convém exemplificar. Considere-se um sistema operativo multitarefa, onde existe a possibilidade de executar várias tarefas concorrentemente. Sempre que acontece uma mudança de contexto, isto é, sempre que é alterada a tarefa em execução é necessário gravar estado da tarefa em execução, e restaurar o estado da tarefa a executar. Contudo, nas tarefas existentes num sistema operativo é possível encontrar tarefas do sistema e tarefas de aplicações. As do sistema como estão associados ao sistema não precisam de gravar tanta informação numa mudança de contexto em comparação com as das aplicações, em que é necessário gravar informação adicional da aplicação. Uma forma tradicional de implementar esta situação consiste em definir uma flag para especificar que tipo de tarefa está representado. Conforme o estado da flag é então decidido o volume de informação a ser guardada ou restaurada. A programação orientada a objetos possibilita uma solução mais simples e mais otimizada. A classe Task pode ser criada como abstrata e as classes SysTask e AppTask como subclasses concretas. Os métodos da subclasse SysTask podem guardar e restaurar o estado de uma tarefa do sistema, enquanto que os métodos da subclasse AppTask guardam a informação adicional das tarefas das aplicações. Exemplos de sistemas operativos orientados a objetos Para a realização desta dissertação é essencial selecionar um sistema operativo orientado a objetos para efetuar o porting e upgrade do mesmo para a família de microcontroladores MCS-51. Assim sendo, torna-se essencial averiguar o trabalho desenvolvido nesta área e avaliar as soluções existentes, para que com base nas suas características e propriedades perceber qual o que mais se adequa aos recursos da arquitetura a utilizar. De seguida são apresentados e caracterizados os sistemas operativos orientados a objetos que o autor considera mais relevantes. Choices O Choices [28, 29] é um sistema operativo orientado a objetos desenvolvido pela 17

36 2.2. Sistemas Operativos Universidade de Illinois em Urbana-Champaign no Estados Unidos da América. Este foi o primeiro sistema operativo a utilizar o paradigma da orientação a objetos, isto é, os componentes do sistema estão encapsulados em classes e apresentam flexibilidade para a gestão e extensibilidade. Desenvolvido em C++, o Choices foi desenhado como uma framework que suporta a maioria das características dos sistemas operativos de propósito geral: gestão de processos, memória virtual, sistema de ficheiros, dispositivos entrada/saída (input/output - I/O) e suporte de rede. Ao nível da gestão de processos, o Choices é um sistema multithread com suporte de vários escalonadores (FIFO 1, LIFO 2, Round Robin 3, etc.). Além disso, oferece mudança de contexto otimizada, isto é, utilizando a herança e subclasse implementa mudança de contexto entre processos de aplicações, processos do sistema ou então processos com interrupção. Quanto aos mecanismos de sincronização de processos, o Choices disponibiliza spin-locks 4 (lock) e busy-wait loops 5 (busy wait) para exclusão mútua, e semáforos (semaphore) para exclusão mútua e sincronização. Relativamente ao sistema de memória virtual, este utiliza page tables independentes da máquina alvo. No que diz respeito ao sistema de ficheiros, o Choices inclui suporte para discos, partições, ficheiros e diretorias conforme os sistemas standard V UNIX, BSD 4.2 UNIX ou MS-DOS. Quanto aos dispositivos I/O, o sistema tem suporte para discos, RAM, dispositivos série, buffers tty, entre outros. A nível de rede suporta ethernet 6, UDP/IP 7 e TCP/IP 8. Além disso, para aqueles que queiram executar aplicações UNIX, o Choices possui uma biblioteca de compatibilidade. Resumindo, o sistema operativo Choices foi o primeiro sistema operativo orientado a objetos desenvolvido para plataformas de propósito geral. Devido à sua arquitetura monolítica e extensa lista de propriedades e características, aliado ao elevado consumo de memória (memory footprint), o autor considera que este sistema operativo 1 FIFO (First-In-First-Out): algoritmo de escalonamento que determina a ordem de execução das tarefas pela ordem de entrada no sistema 2 LIFO (Last-In-First-Out): algoritmo de escalonamento que determina a ordem de execução das tarefas pela ordem inversa de entrada no sistema 3 Round Robin: algoritmo de escalonamento que atribui um tempo fixo a cada tarefa para execução 4 Spin-locks: mecanismo de sincronização de tarefas em que o lock da thread é feito em ciclo até que o recurso esteja disponível 5 Busy-wait loops: técnica de programação em que um processo verifica repetidamente se uma determinada condição é verdadeira 6 Ethernet: padrão (IEEE 802.3) de transmissão de dados para redes locais (LAN) 7 UDP (User Datagram Protocol): protocolo da camada de transporte 8 TCP (Transmission Control Protocol): protocolo da camada de transporte 18

37 Capítulo 2. Estado da Arte não se enquadra no domínio embebido, sobretudo na arquitetura MCS-51 onde os recursos de memória são muito reduzidos. Trion OS O Trion Operating System [30] é um projeto de código aberto cuja intenção passa por criar um sistema operativo moderno de 32/64-bits utilizando os conceitos e ideais da orientação a objetos. Apesar do sistema operativo ainda estar em desenvolvimento, já se encontra disponível para download a versão 0.2. Nesta versão, apesar de prévia, é possível encontrar já a implementação em C++ de uma série de funcionalidades dos sistemas operativos: núcleo, gestão de dispositivos, gestão de memória e gestão de tarefas. Relativamente ao núcleo o sistema é baseado numa estrutura em microkernel com suporte para threads, IPC (Inter Process Comunication), sincronização de tarefas (mutex), interrupções e exceções. Por sua vez, o gestor de dispositivos é responsável por gerir os recursos do kernel, isto é, garante ao sistema o acesso a todos os recursos de hardware, através da detecção de todos os dispositivos usando técnicas de plug and play, informação da BIOS e ficheiros de configuração. Relativamente ao gestor de memória, este pode ser divido em três partes: gestor de memória física, gestor de memória virtual e gestor de memória paginada. O gestor de memória física é responsável por controlar o acesso a toda memória física do sistema, assim como a gestão da pilha. O gestor de memória virtual mantém o controlo da memória virtual usada ou não usada de cada espaço de endereço. O gestor de memória paginada sobretudo grava e carrega páginas de memória em disco. Por fim, o gestor de tarefas é responsável pelo escalonamento das tarefas, isto é, é responsável por carregar novas tarefas e agendar as threads já em execução. Resumidamente, apesar do Trion ser um sistema operativo baseado em microkernel, o autor considera que este também não é uma solução válida porque implementa algumas funcionalidades (memória paginada, memória virtual, técnicas plug and play, ) demasiado complexas para a plataforma alvo. Além disso, este ainda não atingiu sequer uma versão estável e final (apenas está disponível a versão 0.2). BOSS O sistema operativo BOSS [31, 32] é um sistema operativo orientado a objetos de tempo real desenvolvido pela FHG-FIRST, utilizado no satélite BIRD (Bi-Spectral 19

38 2.2. Sistemas Operativos Infrared Detection) [33], e outras aplicações robóticas no espaço. O BOSS foi desenhado com a finalidade de reduzir a complexidade do software de forma a garantir a confiabilidade. O núcleo do sistema operativo foi desenvolvido em C++, e foi efetuado o porting para várias plataformas, nomeadamente para PowerPC, x86 e Atmel AVR. Como o objectivo deste sistema operativo passa pela aplicação em sistemas embebidos, este foi desenvolvido seguindo um modelo arquitetural baseado em microkernel: tem escalonador, gestor de tarefas, mecanismo de sincronização de tarefas (semaphore), gestor de temporização e mailbox. A figura 2.2 [32] apresenta o diagrama de classes do núcleo do BOSS. Figura 2.2: Diagrama de classes do core do sistema operativo BOSS Para além do núcleo principal, o grupo de Sistemas Embebidos (ESRG) da Universidade do Minho [34] foi responsável por introduzir suporte para tolerância a falhas. Assim sendo, foi desenvolvida uma framework de middleware que torna possível a implementação de um conjunto de estratégias de tolerância a falhas, e integrada no sistema operativo BOSS utilizando programação orientada a aspetos (AOP - secção 2.3.5). Em suma, a simplicidade do BOSS assim como a utilização do mesmo em sistemas embebidos de alta fiabilidade, fazem deste sistema operativo orientado a objetos um potencial candidato para o objectivo da presente tese. Todavia, uma vez que o código do sistema operativo é fechado e proprietário, não foi possível utilizar o BOSS no trabalho de dissertação. 20

39 Capítulo 2. Estado da Arte ADEOS ADEOS [35], acrónimo de A Decent Embedded Operating System, é um sistema operativo orientado a objetos baseado em microkernel, desenvolvido por Michael Barr. Desenvolvido em C++, o sistema operativo com cerca de 1000 linhas de código fonte foi desenhado para aplicações em sistemas embebidos com escassez de recursos. Apesar de compacto, este tem as funcionalidades essenciais de um sistema operativo: gestor de tarefas, escalonador, sincronização de tarefas e mudança de contexto. Relativamente ao gestor de tarefas, este encarrega-se de criar novas tarefas, adicionar e remover tarefas da lista de tarefas, e ainda colocar as tarefas em execução. O sistema é multitask pois permite correr várias tarefas simultaneamente. No que diz respeito ao escalonador, este é responsável por decidir que tarefa deve executar em cada instante de tempo e gerir as interrupções. A estratégia de escalonamento é preemptiva e baseada em prioridades, isto é, a cada tarefa é associada uma prioridade e a tarefa que deve ser executada é a de mais alta prioridade da lista de tarefas prontas para execução. Quanto à sincronização de tarefas, é implementado o mecanismo de mutex, disponibilizando os métodos take e release para garantir que num determinado instante de tempo apenas uma tarefa acede a um mesmo recurso partilhado. Finalmente, a mudança de contexto permite guardar e restaurar o estado de uma determinada tarefa sempre que é alterada a tarefa em execução. A mudança de contexto neste sistema operativo é implementada em linguagem assembly específica para a plataforma Concluindo, devido ao seu modelo arquitetural e propensão para o domínio embebido, assim como o facto do código fonte ser livre e com possibilidade de ser facilmente melhorado e expandido, fazem deste sistema operativo a escolha do autor para o trabalho a desenvolver. Este sistema operativo será analisado e explicado ao detalhe na secção Configurabilidade e Variabilidade no Software: técnicas de programação Devido à complexidade dos sistemas atuais, o desenvolvimento de software requer cada vez mais um pensamento estruturado, assim como o uso de mecanismos que permitam desenvolver bem, de modo a minimizar os recursos de hardware necessários, e, simultaneamente, maximizar o desempenho do sistema. Além disso, se 21

40 2.3. Configurabilidade e Variabilidade no Software: técnicas de programação pensarmos no desenvolvimento de software para diferentes aplicações ou produtos, para diferentes plataformas, com diferentes necessidades, é normal que a complexidade de desenvolvimento cresça exponencialmente, e portanto seja necessário arranjar mecanismos, técnicas ou metodologias que permitam gerir toda essa variabilidade e configurabilidade. É neste sentido que o autor apresenta de seguida as principais técnicas e metodologias de programação utilizadas na gestão da variabilidade e configurabilidade do software - (i) compilação condicional, (ii) orientação a objetos, (iii) orientação a componentes, (iv) orientação a funcionalidades, (v) orientação a aspetos, (vi) programação generativa -, procurando apontar os pontos fortes e os inconvenientes de cada uma, de modo a perceber qual a mais adequada para aplicar no refactoring do sistema operativo Compilação Condicional A compilação condicional é uma estratégia de refactoring usada no ambiente de programação C/C++ para o desenvolvimento de software para diferentes plataformas e com diferentes funcionalidades [36]. Do ponto de vista do programador, a compilação condicional, em conjunto com o pré-processador C/C++, é bastante fácil de aprender e aplicar no software configurável. Diretivas de pré-processador C como define, ifdef, ifndef, if, else, etc., são usadas para controlar e gerir zonas ou trechos de código que devem ser incluídos ou excluídos conforme a condição especificada. O exemplo da listagem 2.6 apresenta a utilização de compilação condicional num simples programa escrito em C. Caso o código seja compilado (por exemplo, com o compilador da GNU - GCC [37]) com a macro DEBUG o programa mostra no ecrã a frase DEBUG DEFINED, caso contrário será mostrado DEBUG not defined. #include <stdio.h> int main() #ifdef DEBUG printf( DEBUG DEFINED\n ); #else printf( DEBUG notdefined\n ); #endif return 0; 22

41 Capítulo 2. Estado da Arte } Listagem 2.6: Exemplo da utilização de compilação condicional O sistema operativo Linux é o exemplo típico e magno da aplicação da compilação condicional para gerir a variabilidade e configurabilidade do mesmo. Contudo, como todo o código precisa de ser anotado com diretivas de pré-processamento, este acaba por ficar confuso e ofuscado, tornando extremamente difícil a manutenção e o upgrade. Além disso, uma vez que as anotações não são seguras, isto é, podem ser alteradas com recurso a um simples editor de texto, faz com que esta técnica seja propensa a erros. A simples troca de uma letra numa diretiva de pré-processamento ou de uma macro associada a esta, pode levar a uma inconsistência de funcionamento. Isto atinge uma proporção colossal se pensarmos na quantidade de ficheiros e linhas de código da maioria dos sistemas operativos UNIX atuais. Daí que esta técnica seja muitas vezes criticada na literatura, e tenha sido designada de ifdef considered harmful [38] ou ifdef-hell [39] Orientação a Objetos Outra técnica ou metodologia que pode ser usada para gerir a variabilidade e configurabilidade de um sistema, é a própria orientação a objetos. Suportada pela linguagem C++, este paradigma pode ser usado para implementar as diversas funcionalidades utilizando o polimorfismo dinâmico. Basicamente, consiste em implementar o sistema utilizando o conceito de herança e funções virtuais, gerindo as possíveis configurações em runtime, o que torna o sistema dinâmico e parametrizável. No entanto, este tipo de abordagem resulta numa sobrecarga excessiva de recursos, e numa degradação do desempenho do sistema Orientação a Componentes O conceito da orientação a componentes surgiu com a visão do desenvolvimento de software generalizado. Assim, esta metodologia pretende substituir os sistemas de software monolíticos tradicionais, por componentes de software reutilizáveis e frameworks de componentes em camadas. Desta forma, os componentes aumentam as capacidades das frameworks, enquanto as frameworks fornecem um ambiente de execução para os componentes. No entanto, este termo ainda não é totalmente aceite na comunidade científica, daí que não exista nenhuma linguagem de programação 23

42 2.3. Configurabilidade e Variabilidade no Software: técnicas de programação desenvolvida segundo este paradigma, e apenas exista suporte de orientação a componentes em linguagens orientadas a objetos como o C++, o Java, e, mais concretamente, o Lagoona. [40] Contudo, apesar das vantagens inerentes a esta metodologia, como por exemplo, a reutilização e a especialização, o desenvolvimento de um sistema com a abordagem orientada a componentes proporciona um overhead de recursos, da mesma magnitude da abordagem dinâmica e parametrizável da orientação a objetos. Além disso, ainda tem a desvantagem do compilador não possibilitar a otimização ao nível do componente (devido ao conceito de black-box 9 ), o que acrescenta funcionalidades desnecessárias às aplicações Orientação a Funcionalidades A orientação a funcionalidades, introduzida por Prehofer em 1997 [41], é uma estratégia de software utilizada para combater o problema do crescimento exponencial de classes da orientação a objetos. Em vez de uma estrutura de classes rígida, esta metodologia propõe o desenvolvimento de funcionalidades que descrevem a relação da classe base com as suas extensões, sem utilizar a herança. Isto é, as funcionalidades são semelhantes a subclasses abstratas, contudo com a grande diferença de que as funcionalidades do núcleo da subclasse são separados dos métodos de overwriting da classe base. Desta forma, através da implementação separada dos métodos de overwriting é possível resolver as dependências e interações entre as diversas funcionalidades, isto é, algumas funcionalidades apresentam comportamentos diferentes na presença de outras. Neste sentido, com a programação orientada a funcionalidades é construído um repositório de funcionalidades, que substitui a estrutura rígida e convencional de uma hierarquia de classes tradicional (figura 2.3a [41]). Conforme se pode ver pela figura 2.3b [41], para construir um objeto, em vez do tradicional método de herança, as funcionalidades são adicionadas umas após as outras, com uma determinada ordem. Para adicionar interação e construir uma espécie de hierarquia de classes personalizada, são utilizados os chamados lifters. No trabalho realizado por Prehofer [41] é possível encontrar pormenores de implementação, nomeadamente um exemplo em Java da utilização desta técnica para modelação de pilhas (stacks) com diversas fun- 9 Black-box: dispositivo, sistema, ou objeto que pode ser visto apenas em termos de entradas, saídas e características, sem nenhum conhecimento da implementação e funcionamento interno 24

43 Capítulo 2. Estado da Arte cionalidades. (a) Hierarquia de classes típica (b) Composição de objetos no modelo de funcionalidades Figura 2.3: Orientação a funcionalidades: hierarquia de classes e modelo de funcionalidades Resumindo, comparando com a programação orientada a objetos clássica, a programação orientada a funcionalidades fornece maior modularidade e flexibilidade. A reutilização é simplificada, uma vez que para cada funcionalidade, as características do núcleo são separadas das interações. Daí que esta técnica tenha sido utilizada em diferentes aplicações de diferentes domínios [42], nomeadamente em simuladores de incêndio do exército Norte Americano, em protocolos de rede de alta performance, e ferramentas de verificação de programas Orientação a Aspetos Em 1997, Kiczales et al. [43] foi o responsável por criar o conceito de orientação a aspetos para lidar com um problema de programação designado por cross-cutting. 25

44 2.3. Configurabilidade e Variabilidade no Software: técnicas de programação De forma sucinta, sempre que duas propriedades a programar componham múltiplos elementos, e, simultaneamente, necessitem de coordenação, então diz-se que estas são transversais ou que se cross-cut uma à outra. Foi na tentativa de simplificar este problema, e melhorar o desenvolvimento e manutenção dos sistemas de software, que Kiczales apresentou o conceito de aspetos. Um exemplo típico onde este problema de cross-cutting acontece, e onde é possível aplicar a programação orientada a aspetos, é nos sistemas de autenticação. Como a estratégia de loggin afeta necessariamente inúmeras partes do sistema, então diz-se que o logging é transversal a todas as classes e métodos de autenticação. Por outras palavras, a programação orientada a aspetos (aspect oriented programming - AOP) procura resolver o problema que geralmente uma única dimensão da decomposição funcional não é suficiente para implementar todos os aspetos de um programa de forma modular [44]. Isto significa que o código que resulta de uma única decisão de design é amplamente disseminado por todo o sistema, ou seja, este não pode ser encapsulado numa única função, classe ou método. Este tipo de código é designado por aspect code. Um exemplo muito referenciado para ilustrar este efeito consiste no código para efetuar sincronização em programas não-sequenciais. Apesar de no design ser possível especificar onde tem que ser introduzido o código de sincronização, e o que este deve fazer, não é possível encapsular de forma transparente. Portanto, a AOP ajuda neste dilema, pois o ambiente de desenvolvimento desta abordagem permite implementar o cross-cutting em unidades modulares (aspetos) e uma ferramenta - aspect weaver - insere os fragmentos de código, derivados do código aspeto, onde estes são precisos. Estes pontos de inserção são designados por joint points. A figura 2.4 [44] ilustra, simplificadamente, como é que o código aspeto é embutido pelo aspect weaver no código dos componentes. Para possibilitar o desenvolvimento e implementação de código aspeto nas ferramentas de desenvolvimento C++, é necessário utilizar uma extensão para a linguagem, como AspectC++ [44], que atua como um pré-processador source-to-source Programação Generativa Todas as técnicas e metodologias de customização mencionadas até ao momento apresentam algumas limitações ou inconvenientes, nomeadamente, propensão a erros, consumo excessivo de recursos, debilidades no desempenho, e necessidade de ferramentas adicionais demasiado precoces no estágio de desenvolvimento. 26

45 Capítulo 2. Estado da Arte Figura 2.4: Junção do código aspeto no código dos componentes Neste sentido, e numa tentativa de resolver a maioria dos problemas apontados anteriormente, uma outra forma de gerir a configurabilidade e variabilidade do software consiste em usar técnicas da programação generativa, nomeadamente C++ template metaprogramming (C++ TMP) [6, 7, 45, 46]. Esta metodologia acaba por ser classificada, em parte, como uma linguagem funcional, pois é processada pelo compilador durante a fase de instanciação dos templates, ou seja, é processada em tempo de compilação e não em tempo de execução. Desta forma, é possível efetuar geração de código, cálculo de constantes, seleção de tipos, etc., e ao mesmo tempo gerar apenas as funcionalidades pretendidas, garantindo assim código otimizado e ajustado às necessidades da aplicação. Por outras palavras, como o compilador atua momentaneamente como um interpretador, todo o processamento é realizado em tempo de compilação, resultando em código otimizado e específico para a configuração requerida, garantindo assim uma melhor gestão dos recursos e desempenho do sistema. Contudo, apesar da potencialidade desta metodologia para a gestão de variabilidade e customização de sistemas, esta apresenta um inconveniente. Caso algo de errado aconteça durante a fase de compilação, o compilador gera mensagens que podem ser demasiado difíceis de interpretar, o que pode tornar crítico e moroso o processo de desenvolvimento. No entanto, existem já mecanismos para minimizar o problema: por um lado, (i) usar técnicas que permitam a geração de mensagens de erros custo- 27

46 2.4. Conclusões mizadas; por outro lado, (ii) utilizar compiladores com melhor suporte ao template metaprogramming. Esta técnica será revista e explicada com mais detalhe na secção Conclusões Este capítulo, para além de fundamentar e familiarizar a leitura do documento com os termos técnicos e conceitos das diferentes temáticas adjacentes ao trabalho - programação orientada a objetos, sistemas operativos, e variabilidade e configurabilidade no software -, permitiu tomar duas decisões fundamentais para o sucesso da dissertação. A primeira está centrada na escolha do sistema operativo orientado a objetos. Segundo a avaliação do autor, o sistema operativo ADEOS (código aberto) é a escolha mais acertada para os recursos da plataforma alvo. Por sua vez, a decisão de utilizar TMP para gerir a variabilidade do sistema operativo de forma estática, promete a geração de código otimizado, sem overhead e deterioração do desempenho do sistema. 28

47 Capítulo 3 Especificação do Sistema O capítulo anterior permitiu expor os conceitos essenciais ao enquadramento da temática da dissertação. Além disso, foi discutido e definido o sistema operativo a adotar, assim como a metodologia e abordagem para a gestão da variabilidade do sistema. Neste capítulo, por sua vez, serão explicados, numa aproximação bottom-up, cada uma das camadas e componentes que compõe a base do sistema a implementar. Assim, primeiro será explicada a arquitetura do microcontrolador Memória, periféricos, conjunto de instruções, são alguns dos conceitos essenciais para a compreender a sua arquitetura. Depois disso, o sistema operativo ADEOS será revisto, com o objetivo de perceber detalhadamente o código implementado por Michael Barr ao nível de escalonamento, tarefas e sincronização das mesmas. A técnica de C++ template metaprogramming também será novamente abordada, de modo a explicar detalhes de implementação, bem como exemplos de aplicação. No fim do capítulo serão apresentadas algumas particularidades do compilador C++ da IAR para o 8051, que o autor considera importantes para o sucesso do trabalho da presente dissertação. 3.1 Microcontrolador 8051 Em 1981, a Intel Corporation [47] apresentou um microcontrolador designado por Seguindo uma arquitetura Harvard, isto é, memória de código separada fisicamente da memória de dados, este microcontrolador, na sua versão clássica, possuía 128-byte de RAM (memória de dados), 4-kbyte de ROM (memória de código), dois temporizadores, uma porta série, e quatro portas (8-bit) entrada/saída de propósito geral. O 8051 é um microcontrolador de 8-bit, o que significa que a unidade de proces- 29

48 3.1. Microcontrolador 8051 samento apenas consegue processar 8-bit de dados a cada instante de tempo. Dados com tamanho superior a 8-bit tem que ser divididos e processados ao byte. A figura 3.1 apresenta o diagrama de blocos dessa versão do microcontrolador. [48] Figura 3.1: Diagrama de blocos do microcontrolador 8051 clássico Este microcontrolador tornou-se ainda mais popular quando a Intel Corporation permitiu que outros fabricantes o reproduzissem, na condição de que estes garantissem compatibilidade do código e instruções do 8051 original. Isto levou ao aparecimento de muitas versões do microcontrolador, com diferentes configurações de velocidades, tipo e capacidade da memória de código e dados, e periféricos. Os microcontroladores atuais baseados no núcleo do 8051 têm várias características importantes, como interfaces de comunicações I 2 C (secção 4.2.2), SPI (secção 4.2.2), CAN 1, conversores analógico-digital (ADC), conversores digital-analógico (DAC), geradores PWM (secção 4.2.2), e memória de programa Flash auto-programável. Inclusive, recentemente a Texas Instruments [49] lançou uma família de microcontroladores baseado no núcleo do 8051, que possui on-chip um transceiver de rádio frequência para comunicações sem fios sub-1ghz (p.e. CC1111 [50]) e 2.4GHz (p.e. CC2530 [51]). 1 CAN (Controller Area Network): protocolo de comunicação desenhado especialmente para a indústria automóvel 30

49 Capítulo 3. Especificação do Sistema Arquitetura de Memória O microcontrolador 8051 tem quatro memórias distintas: (i) memória de dados interna (RAM interna); (ii) registos de funções especiais (SFR - special function registers, RAM interna); (iii) memória de programa ou de código (Flash interna ou ROM externa); (iv) e memória de dados externa (RAM externa). A versão original do 8051 possui 128-byte de memória de dados interna que podem ser endereçados direta ou indiretamente (secção 3.1.5). Nos endereços de 00h a 1Fh desta memória, estão localizados os bancos de registos. Este possui quatro bancos, sendo, por defeito, selecionado o banco 0. No banco de registo selecionado estão sempre mapeados oito registos de trabalho (R0 a R7) disponíveis ao programador. Por sua vez, do endereço 20h a 2Fh estão disponíveis 128 localizações endereçáveis ao bit. Isto significa que com uma única instrução pode-se executar operações booleanas sobre os bits individuas desta área. As restantes posições de memória, 30h a 7Fh, estão livres, o que significa que estão disponíveis para armazenar dados e variáveis definidas pelo programador. Existem outras versões deste microcontrolador que disponibilizam mais 128-byte de dados de propósito geral. Como estes 128-byte estão nos endereços 80h a FFh, ou seja, os mesmos endereços da área do SFR, o microcontrolador faz essa distinção através do endereçamento utilizado. Se a instrução utilizar endereçamento direto acede ao SFR. Caso a instrução utilize endereçamento indireto acede aos 128-byte de dados extra. Todos os registos internos do 8051 estão mapeados nos 128-byte superiores da memória de dados interna. Assim sendo, nos endereços 80h a FFh está localizada a área do SFR, que contém todos os registos do 8051, com exceção dos bancos de registos de propósito geral R0 a R7. No 8051 original estão apenas definidos 21 endereços, no entanto nos derivados mais recentes desta família a grande maioria dos endereços do SFR está já ocupada. Estes registos permitem o acesso e o controlo de todos os periféricos internos do A memória de programa é destinada a armazenar o código e constantes da aplicação. Assim sendo, a memória é apenas de leitura e tipicamente é implementada em memória ROM. Esta pode estar dentro ou fora do chip, com capacidade até 64kbyte, dependendo do modelo usado. Tal como já foi referido, algumas variantes do 8051 possuem memória flash em substituição da memória ROM. A memória externa de dados pode ser utilizada para armazenar dados e variáveis do programador, ou simplesmente para implementar uma segunda área do SFR. Esta 31

50 3.1. Microcontrolador 8051 memória pode ser acedida através de acesso indireto, utilizando uma instrução especial, de mnemónica MOVX. Atualmente, algumas versões do microcontrolador colocam parte desta memória externa dentro do chip. O espaço de memória permitido pela arquitetura é de 64k-byte, tendo três barramentos disponibilizados para o efeito: barramento de endereços de 16-bit; barramento de dados de 8-bit; barramento de controlo de 3-bit. A figura 3.2 resume e ilustra o mapa de memória genérico das diferentes variantes do microcontrolador Figura 3.2: Mapa de memória do 8051 genérico Registos Básicos Para além dos quatro bancos de registos de uso geral já mencionados anteriormente (R0 a R7), o microcontrolador dispõe de outros registos básicos de significativa relevância. O registo A (Accumulator) e o registo B, ambos de 8-bits, são utilizados para operações aritméticas. O registo PSW (Program Status Word) contém os bits de estado que refletem o estado atual do CPU, nomeadamente as flags de carry (C), carry auxiliar (CA), seleção do banco de registos (RS0 e RS1), overflow (OV) e paridade (P). O registo IE (Interrupt Enable) permite configurar e gerir as interrupções. 32

51 Capítulo 3. Especificação do Sistema O registo SP (Stack Pointer) é utilizado como apontador para a pilha. O registo DPTR (Data Pointer), de 16-bit, é muito útil para endereçar memória de dados externa e memória de código. Finalmente o registo PC (Program Counter), também de 16-bit, contém o endereço de memória de programa da próxima instrução a ser executada Periféricos O microcontrolador 8051, na sua versão clássica, inclui essencialmente três grupos de periféricos: (i) portas entrada/saída digital; (ii) contadores/temporizadores de 16- bit; e (iii) porta série. As quatro portas de entrada/saída digital possuem quatro registos de 8-bit, mapeados no SFR, que permitem controlá-las: P0, P1, P2 e P3. Cada um destes registos possui oito latches 2 e hardware de interface às saídas (output drivers) e de leitura das entradas (input buffers) que permitem implementar as funcionalidades necessárias a uma porta de entrada/saída digital. As oito linhas de cada uma destas portas I/O podem ser tratadas individualmente, de modo a realizar a interface a dispositivos de 1-bit (LEDs 3, ataque de MOSFETs 4, etc.), ou então como unidades para realizar a interface paralela de 8-bit a outros dispositivos (display LCD, teclado, etc.). Relativamente às unidades de contagem, contador/temporizador 0 e contador/- temporizador 1, estes podem ser configurados para funcionar como temporizador ou contador de eventos. Quando configurados como temporizadores, os registos de contagem THx e TLx (onde x corresponde a 0 ou 1 dependendo do número do temporizador), são incrementados a cada ciclo máquina através de um sinal cuja frequência é 1/12 da frequência do oscilador interno do CPU. Quando configurados como contadores, os registos de contagem são incrementados na transição descendente do sinal à entrada do pino P3.4 e P3.5. A porta série existente na família MCS-51 permite a transferência no modo fullduplex 5 e pode funcionar em vários modos e frequências. A sua principal função consiste na conversão paralelo-série dos dados a serem transmitidos, e na conversão série-paralelo dos dados. O hardware da porta série pode ser acedido através dos 2 Latches: circuito sequencial biestável assíncrono capaz de armazenar um bit de informação 3 LED (Light-Emitting Diode): semicondutor (díodo) emissor de luz 4 MOSFET (Metal Oxide Semiconductor Field Effect Transistor): transistor de efeito de campo 5 Full-duplex: permite comunicação (transmissão e recepção) em ambos os sentidos simultaneamente 33

52 3.1. Microcontrolador 8051 Tabela 3.1: Vetores de interrupção na família MCS-51 Interrupção Flag de Interrupção Bit SFR Endereço SFR RESET RST 00h Externa 0 IE0 TCON.1 03h Timer 0 TF0 TCON.5 0Bh Externa 1 IE1 TCON.3 13h Timer 1 TF1 TCON.7 1Bh Porta série RI ou TI SCON.0 ou SCON.1 23h pinos TxD e RxD e apresenta um buffer que permite a receção de um segundo byte, antes da leitura do primeiro. Pode-se configurar a porta série para transmissão com frequência fixa, derivado do oscilador interno, ou variável, através da programação do temporizador 1 (nas novas variantes do 8051 o temporizador 2 também pode ser utilizado para gerar a frequência de transmissão). [52] Interrupções O 8051 original apresenta duas fontes de interrupções externa, duas interrupções das unidades contadoras/temporizadoras, e uma interrupção da porta série. Existem três registos que fornecem o controlo total sobre todas as interrupções do 8051: (i) registo IE, que controla a ativação das interrupções; (ii) registo IP (Interrupt Priority), que permite configurar a prioridade individual das fontes de interrupções; e (iii) o registo TCON (Timer Control), que permite configurar a forma de acionamento das duas interrupções externas. A tabela 3.1 apresenta algumas informações sobre as várias fontes de interrupção, entre os quais os endereços das ISR, as flags de interrupção associadas e as SFR onde se encontram as flags. Na ocorrência de uma interrupção e da aceitação da mesma pelo processador, o programa principal é interrompido, desencadeando o seguinte conjunto de ações: (i) é concluída a execução da instrução atualmente em execução; (ii) o endereço de retorno do PC é guardado na pilha; (iii) o estado atual da interrupção é guardado internamente; (iv) as interrupções são desativadas; (v) o PC é carregado com o endereço do vetor da ISR; e, finalmente, (vi) a ISR é executada, sendo posteriormente terminada com a instrução de RETI. 34

53 Capítulo 3. Especificação do Sistema Tabela 3.2: Modos de endereçamento do 8051 Modo de endereçamento Endereçamento imediato Endereçamento direto Endereçamento direto por registo Endereçamento indireto por registo Endereçamento implícito Endereçamento indexado Endereçamento relativo Endereçamento absoluto Endereçamento longo Código exemplo MOV A,#55H MOV A,50H MOV A,R7 MOV PUSH ACC SJMP loop0 ACALL loop1 LJMP loop Arquitetura do Conjunto de Instruções A arquitetura do conjunto de instruções (ISA - Instruction Set Architecture) define a interface entre o programador e o processador, isto é, fornece ao programador toda a informação necessária para a interação e comunicação com o processador. Por outras palavras, o ISA descreve o conjunto de instruções assembly suportadas pelo processador, juntamente com as informações relativas aos registos acessíveis ao programador, interação com a memória e gestão das interrupções. Modos de Endereçamento Independentemente do tipo de ISA, o processador, quando acede a um operando para efetuar uma operação de leitura ou escrita, deve especificar como é que os endereços de memória e registos devem ser representados e interpretados. Uma instrução em linguagem assembly pode usar um de vários modos de endereçamento, a partir do qual o CPU gera o endereço especificado para, posteriormente, aceder ao subsistema de memória. A tabela 3.2 apresenta os nove modos de endereçamento disponíveis no 8051, assim como algumas instruções onde estes se aplicam. O modo de endereçamento imediato utiliza constantes de 8 ou 16 bits como operando fonte. Esta constante é especificada diretamente na instrução, ao invés de ser especificada por registo ou por endereço de memória. No modo de endereçamento direto, a instrução define o endereço do operando como uma constante e o processador acede à localização de memória. Este modo é tipicamente utilizado para aceder à área de memória do SFR. O modo de endereçamento direto por registo é idêntico ao modo de endereçamento 35

54 3.1. Microcontrolador 8051 direto, exceptuando o facto de ser especificado um registo (isto é, um meio endereço) e nunca um endereço de memória, ou seja, é o registo que contém o operando. No modo de endereçamento indireto, a instrução especifica o endereço de uma localização de memória que contém o endereço do operando. Isto significa que requer duas referências à memória para ler o operando. Apenas os registos R0, R1 e DPTR podem ser utilizados. Este modo de endereçamento é muito utilizado para implementar o conceito de apontadores, visto o 8051 não implementar o modo de endereçamento indirecto por memória (apenas suporta endereçamento indirecto por registo). O modo de endereçamento implícito não especifica explicitamente um operando pois tem-se sempre associado um determinado registo ou a pilha. Apesar de este modo de endereçamento não se aplicar diretamente no 8051, as instruções PUSH e POP especificam implicitamente o topo da pilha como sendo o outro operando. O modo de endereçamento por deslocamento, nomeadamente base por registo, é especialmente útil quando se necessita aceder a dados em memória de código. Neste modo especificam-se dois operandos, onde um deles contém um endereço de memória e o outro o deslocamento relativo ao endereço de memória. O modo de endereçamento relativo é utilizado por algumas instruções de salto (por exemplo, SJMP) e salto condicional (por exemplo, JNZ). O operando fornecido pela instrução contém um offset que será adicionado ao endereço da instrução atual por forma a gerar o endereço efetivo. Este destino efetivo deve-se encontrar entre -128 e +127 bytes da instrução atual dado o comprimento de 8-bit do offset. O modo de endereçamento absoluto está associado às instruções ACALL e AJMP. Estas são instruções de 2-byte, que especificam um endereço absoluto de 11-bit. Atendendo ao fato dos 5-bit mais significativos do PC (16-bit) não serem modificados, estas instruções permitem apenas saltos dentro de páginas de 2k-byte, onde a memória de código se encontra logicamente dividida em 32 páginas. O modo de endereçamento longo é utilizado através das instruções LCALL e LJMP. Estas são instruções de 3-byte em que os últimos 2-byte especificam um endereço de destino de 16-bit. Desta forma é possível percorrer os 64k-byte de memória de código. Tipos de Instruções O microcontrolador 8051 disponibiliza 255 instruções assembly [53], agrupadas em três grupos funcionais: (i) instruções lógicas e aritméticas; (ii) instruções de 36

55 Capítulo 3. Especificação do Sistema transferência de dados; e (iii) instruções de controlo. As instruções lógicas e aritméticas caracterizam-se por modificarem o valor do operando destino. Instruções que efetuam a soma, subtração, multiplicação, divisão ou deslocamento, são classificadas como instruções aritméticas, enquanto as que efetuam o e-lógico, ou-lógico, xor-lógico e complemento, são designadas por instruções lógicas. As instruções do tipo set ou clear, podem ser classificadas como lógicas ou aritméticas. As operações aritméticas tem a particularidade de afectarem as flags do processador, nomeadamente, carry, carry auxiliar, overflow, e paridade. [52] Por sua vez, as instruções de transferência de dados não modificam os dados originais, pois estes não são removidos da sua localização, apenas são copiados para uma nova localização. As instruções que efetuam a transferência de dados podem ser divididas em três grandes tipos: (1) MOV destino, fonte; (2) PUSH fonte ou POP fonte; e (3) XCH destino, fonte. [52] Finalmente, as instruções de controlo alteram o fluxo de execução do programa e efetuam o fetch da próxima instrução de uma localização de memória diferente do endereço consecutivo. Normalmente, alteram o valor do registo PC com um endereço de uma instrução diferente da instrução consecutiva, e o próximo ciclo de fetch usa este novo endereço colocado no registo PC para obter a próxima instrução. Tal como as instruções de transferência de dados, também as instruções de controlo podem ser divididas em três tipos: (1) salto condicional; (2) salto incondicional; e (3) gestão de subrotinas e interrupções. [52] 3.2 ADEOS: A Decent Embedded Operating System Acrónimo de ADEOS, A Decent Embedded Operating System é um sistema operativo orientado a objetos desenvolvido em C++ por Michael Barr. Foi desenvolvido para aplicações embebidas, daí que o número de linhas do código fonte seja inferior a A maioria do código foi implementado independente da arquitetura e seguindo o paradigma de abstração da programação orientada a objetos. Por isso, a maioria das funcionalidades estão estruturadas em classes, sendo apenas escritas em linguagem assembly três rotinas específicas ao processador [54]. Portanto, para fazer o porting do sistema operativo para a plataforma 8051, apenas devem ser re-implementadas estas três rotinas. 37

56 3.2. ADEOS: A Decent Embedded Operating System As funcionalidades implementadas pelo sistema operativo são mínimas, mas as essenciais para o correto funcionamento do mesmo: gestor de tarefas (Task), escalonador (Sched) e sincronização de tarefas (Mutex). A figura 3.3 apresenta o diagrama de classes do ADEOS. Figura 3.3: Diagrama de classes do ADEOS Tarefas Quando se fala de um sistema operativo multitarefa (multitasking) significa que o sistema operativo possibilita a execução de várias tarefas ao mesmo tempo. No entanto, em arquiteturas com um único processador (single-processor) e núcleo (singlecore), como é o caso da família MCS-51, as tarefas não são executadas paralelamente, mas sim de forma pseudo-paralela. 38

57 Capítulo 3. Especificação do Sistema Desta forma, o sistema operativo é responsável por decidir que tarefa executará em instante de tempo. Portanto, durante a comutação da tarefa este deve guardar a informação sobre o estado de cada tarefa, designado como contexto da tarefa (context). O mecanismo de comutação de contexto guarda o estado do processador antes de outra tarefa assumir o controlo do mesmo, e de seguida restaura o estado da tarefa selecionada para execução. Esse estado consiste basicamente no apontador para a próxima instrução a ser executada, no endereço do topo da pilha da tarefa, e o conteúdo dos registos e flags do processador. Neste sentido, para manter as tarefas e respetivos contextos organizados, o sistema operativo retém a informação de cada tarefa. Essa informação é guardada sob a forma de estruturas de dados designados por task control block (TCB). No ADEOS a classe (Task) (listagem 3.1) é uma implementação C++ do TCB. class Task public: Task (void ( function)(), Priority, int stacksize); TaskId id; Priority priority; TaskState state; Context context; int pstack; Task pnext; void ( entrypoint)(); }; private: static TaskId nextid; Listagem 3.1: Declaração da classe Task Nesta classe importa explicar os atributos id, priority, state, context, pstack, pnext. O id contém um número inteiro (entre 0 e 255) que identifica a tarefa. O priority identifica a prioridade da tarefa. O state informa sobre o estado da tarefa, isto é, se a tarefa está em execução, se está pronta a executar ou se está em espera. O context é a estrutura de dados que contém o estado do processador da última vez que a tarefa teve acesso à execução. O pstack é um apontador para o topo da pilha da tarefa (isto é, stack frame da tarefa). Finalmente, o pnext é um apontador para a próxima entrada TCB de uma das possíveis tarefas, estando a lista ligada ordenada por prioridade. 39

58 3.2. ADEOS: A Decent Embedded Operating System Estado das Tarefas Conforme foi referido anteriormente apenas uma tarefa pode usar o processador em cada instante de tempo. Portanto, essa tarefa é designada como a tarefa em execução (running), e é a única que pode ter associado esse estado em cada instante de tempo. Por sua vez, tarefas que estão prontas a executar mas que não estão a usar o processador encontram-se no estado ready, enquanto que tarefas que estão bloqueados à espera de um evento externo são tarefas no estado waiting. A figura 3.4 ilustra a relação entre os três estados que podem ser associados a uma tarefa. Figura 3.4: Relação dos estados das tarefas no ADEOS Uma transição entre o estado ready e running ocorre sempre que o escalonador do sistema operativo seleciona uma nova tarefa para executar. Por outras palavras, a tarefa que estava em execução passa para o estado ready, e a nova tarefa passa para execução (running). Desde que uma tarefa esteja em execução, esta apenas transita desse estado para outro se for forçada pelo escalonador do sistema operativo ou então se tiver de esperar que um determinado evento externo ocorra. Nesse caso a tarefa é colocada em estado waiting e uma nova tarefa é colocada em execução. Logo que esse evento externo ocorra a tarefa é então colocada no estado ready. Resumindo, embora possa haver várias tarefas no estado ready e waiting, apenas uma e só uma tarefa pode estar no estado running em cada instante de tempo. Mecanismos das Tarefas Qualquer classe definida numa linguagem de programação tem sempre associada um conjunto de rotinas. Neste sentido, também a class Task tem o seu próprio grupo de rotinas que permitem fazer a gestão das tarefas. No entanto, a interface das tarefas no ADEOS é mais simples que na maioria dos sistemas operativos, pois 40

59 Capítulo 3. Especificação do Sistema a única funcionalidade disponível consiste em criar objetos dessa classe. Isto porque o ADEOS distingue-se dos demais RTOS no mecanismo de controlo de execução das tarefas. Este é baseado numa máquina de estados com apenas três estados, ao contrário da maioria dos RTOS que apresentam um quarto estado (por exemplo Dead) indicando a conclusão de execução da tarefa. Este estado é que indica que a tarefa deve ficar fora do processo de escalonamento. No entanto, no ADEOS não é obrigatório que a rotina da tarefa seja implementada em corpo infinito. Como a primeira execução da tarefa é efetuada utilizando a função Run (listagem 3.2), se o corpo da tarefa não for implementada com um ciclo infinito, assim que a esta termine o sistema operativo retorna à função run, que é responsável por excluir a tarefa da lista de tarefas prontas a executar, e colocar uma nova tarefa em execução (ponto de escalonamento). void run(task ptask) // Start the task, by executing the associated function. ptask >entrypoint(); } entercs();////// Critical Section Begin // Remove this task from the scheduler s data structures. os.readylist.remove(ptask); os.prunningtask = NULL; // Free the task s stack space. delete ptask >pstack; os.schedule(); // Scheduling Point // This line will never be reached. Listagem 3.2: Função de iniciação das tarefas - run Voltando de novo ao construtor da classe Task (listagem 3.3) este recebe três parâmetros de entrada. O primeiro parâmetro, function, é um apontador para a função a ser executada pela nova tarefa. O segundo parâmetro, p, é um número único entre 1 e 255 que representa a prioridade da nova tarefa relativamente às outras tarefas no sistema. Estes números são usados pelo escalonador quando seleciona uma nova tarefa para execução (255 representa a prioridade máxima). Por fim, o terceiro parâmetro, stacksize, consiste no número de bytes que devem ser reservados para a pilha da tarefa. Task::Task(void ( function)(), Priority p, int stacksize) stacksize /= sizeof(int);// Convert bytes to words. entercs();////// Critical Section Begin 41

60 3.2. ADEOS: A Decent Embedded Operating System }; // Initialize the task specific data.... // Initialize the processor context. contextinit(&context, run, this, pstack + stacksize); // Insert the task into the ready list. os.readylist.insert(this); os.schedule();// Scheduling Point exitcs();////// Critical Section End Listagem 3.3: Construtor da classe Task Relativamente ao corpo do construtor, pode-se verificar que a rotina é envolvida por duas macros: entercs e exitcs. O bloco de código entre estas duas macros é designado por secção critica. Uma secção crítica é um pedaço de programa que deve ser executado de forma atómica, ou seja, o código deve ser executado sequencialmente e sem interrupções. Assim sendo, basicamente o que essas macros fazem é habilitar e desabilitar as interrupções de forma a garantir a atomicidade do código a ser executado. Nesse bloco de código atómico importa referenciar especialmente a chamada de três funções: contextinit, os.readylist.insert e os.schedule. A rotina de contextinit estabelece o contexto inicial de uma tarefa. A segunda rotina adiciona a tarefa à lista de tarefas prontas a executar do sistema operativo. Esta lista é um objeto do tipo TaskList, que consiste numa lista ligada de tarefas ordenada por prioridade. Finalmente, a rotina os.schedule invoca o escalonador do ADEOS de forma a decidir que tarefa deve ser colocada em execução Escalonador O escalonador é a fração do sistema operativo que decide que tarefa será escolhida para execução em cada instante de tempo. No entanto, o método de decisão ou, por outras palavras, o algoritmo de escalonamento pode ser diferente. Nos sistemas operativos de tempo-real é necessário que a estratégia de escalonamento permita que as tarefas mais importantes entrem em execução com a menor latência possível. Daí que a maioria dos RTOS utilizem algoritmos de escalonamento baseado em prioridades com preempção. Quando um algoritmo baseado em prioridades é implementado, é necessário implementar também uma estratégia de desempate. Por outras palavras, é necessário estabelecer uma regra que permita definir que tarefa deve ser executada no caso de 42

61 Capítulo 3. Especificação do Sistema existirem várias tarefas com a mesma prioridade. A estratégia mais usada nesses casos é o algoritmo round robin. No caso do ADEOS, tal como já foi referido, o escalonador também é baseado em prioridades. No entanto, por questões de simplicidade a estratégia de desempate implementada consiste no algoritmo FIFO. Pontos de Escalonamento Os pontos de escalonamento (scheduling points) podem ser designados como eventos do sistema operativo que desencadeiam a invocação do escalonador. Neste sentido, podemos desde já estabelecer dois pontos de escalonamento: na criação de tarefas e na eliminação das mesmas. Na ocorrência destes eventos, o método os.schedule é invocado de forma a selecionar a próxima tarefa a ser executada. Se a tarefa atualmente em execução ainda for a de maior prioridade, então esta continuará a usar o processador. Caso contrário, a tarefa de maior prioridade da lista de tarefas (readylist) será executada. A eliminação da tarefa é feita pelo sistema operativo utilizando o método run explicado anteriormente. Isto significa que o ADEOS não fornece nenhum serviço para de forma explicita matar uma tarefa. Um terceiro ponto de escalonamento acontece aquando do clock-tick. O clocktick é um evento periódico desencadeado pelo trigger da interrupção do temporizador. No ADEOS, este é responsável por acordar as tarefas que estão à espera que um determinado temporizador por software termine a contagem. Na verdade, a utilização de temporizadores por software é uma funcionalidade comum em sistemas operativos embebidos. Com a ocorrência do clock-tick o sistema operativo decrementa e verifica os temporizadores por software ativos, e caso algum finalize a contagem todas as tarefas colocadas em estado waiting à espera da temporização são comutadas para o estado de ready. De seguida, o escalonador é invocado e é verificada se alguma das novas tarefas acordadas tem associada uma prioridade mais elevada que a tarefa em execução antes da interrupção temporal. Ready list O escalonador, para gerir as tarefas que estão prontas a ser executadas, usa uma estrutura de dados chamada readylist, implementada com uma lista ligada ordenada pela prioridade da tarefa. Portanto, na cabeça da lista está sempre a tarefa pronta a executar com a prioridade mais elevada, e na cauda da lista a tarefa com prioridade mais baixa. A figura 3.5 ilustra a lista ligada explicada. A principal vantagem da lista 43

62 3.2. ADEOS: A Decent Embedded Operating System ligada ordenada é a facilidade e rapidez com que o escalonador seleciona a próxima tarefa a executar, pois é sempre a tarefa no topo da lista. Figura 3.5: Ilustração da lista de tarefas prontas a executar (readylist) Tarefa Idle Na eventualidade de não haver tarefas prontas a executar (no estado ready) quando o escalonador é chamado, é então necessário garantir a existência de uma tarefa para ser executada. Essa tarefa é designada por idle task e é semelhante em muitos dos RTOS. Consiste simplesmente num ciclo vazio infinito que mantém ocupado o processador a saltar sempre para a mesma instrução. No entanto, em sistemas operativos mais avançados, esta tarefa é explorada na gestão do consumo, para evitar desperdícios de energia desnecessários. Inclusive, isso acontece no trabalho da presente dissertação (secção 4.2.3). No ADEOS, a idle task tem associado um identificador e uma prioridade válidos, sendo zero em ambos os casos. Assim sendo, essa tarefa está sempre presente na readylist, e devido à sua baixa prioridade, é a tarefa da cauda da lista. Desta forma, o escalonador executará esta tarefa apenas quando não existirem mais tarefas prontas para execução. Algoritmo Escalonamento Uma vez que é usada uma lista ligada ordenada para gerir as tarefas prontas a executar, o algoritmo de escalonamento torna-se bastante simples de implementar. Em poucas palavras, este simplesmente verifica se a tarefa em execução e a tarefa do topo da lista são a mesma. Se são, então não é preciso escalonar. Caso contrário, é necessário comutar de contexto e colocar em execução a tarefa do topo da readylist. A implementação C++ do algoritmo de escalonamento do ADEOS pode ser visto na listagem 3.4. void Sched::schedule(void) 44

63 Capítulo 3. Especificação do Sistema... } // If there is a higher priority ready task, switch to it. if (prunningtask!= readylist.ptop) poldtask = prunningtask; pnewtask = readylist.ptop; pnewtask >state = Running; prunningtask = pnewtask; if (poldtask == NULL) contextswitch(null, &pnewtask >context); } else poldtask >state = Ready; contextswitch(&poldtask >context, &pnewtask >context); } } Listagem 3.4: Método schedule da classe Sched Sincronização de Tarefas Num sistema operativo multitarefa, a maioria das tarefas executadas concorrentemente não funcionam como entidades completamente independentes. Muitas vezes, as várias tarefas trabalham cooperativamente no sentido de resolver problemas de maior complexidade, daí que necessitem de comunicar entre elas para sincronizar as suas atividades. Por exemplo, num sistema de controlo em que se faz amostragem de dados e se aplica controlo PID (Proportional-Integral-Derivative), a tarefa responsável pela aplicação do algoritmo de controlo não pode ser executada até que a amostra seja fornecida pelo ADC. Uma forma de resolver esse problema é usar um mecanismo designado por mutex. Assim sendo, os mutexes são disponibilizados pelo sistema operativo para auxiliar na sincronização de tarefas. No entanto, não são a única forma de o fazer. Existem outros mecanismos de sincronismo e comunicação, como os semaphores, message queues 6 e shared memory 7. Na verdade, o mutex é um tipo especial de semaphore 6 Message queue: mecanismo de comunicação entre tarefas que utiliza queues para enviar mensagens entre os processos/threads 7 Shared memory: mecanismo que utiliza porções reservadas de memória para a troca de dados entre tarefas 45

64 3.2. ADEOS: A Decent Embedded Operating System designado binário ou mesmo mutuamente exclusivo. Em poucas palavras, um mutex pode ser definido como um sinalizador multitarefa, isto é, havendo um recurso partilhado por mais que uma tarefa, logo que uma das tarefas associe e sinalize esse recurso com o mutex, então mais nenhuma das tarefas pode aceder a esse recurso até que a tarefa desative o sinalizador. No caso do ADEOS, para sincronização de tarefas o mecanismo disponível são os mutexes. Utilizando a classe Mutex é possível criar e destruí-los, e ainda ativar ou desativá-los. Estas duas últimas operações são fornecidas pelos métodos take e release. O processo de criação de um novo mutex (listagem 3.5) é bastante simples: todos os mutexes são criados com estado available, e associados a uma lista ligada de tarefas em estado waiting inicialmente vazia. No entanto, claro que uma vez criado um mutex é necessário arranjar alguma forma de mudar o seu estado. Neste sentido, foram implementadas no ADEOS os métodos take e realese. Mutex::Mutex() entercs();////// Critical Section Begin state = Available; waitinglist.ptop = NULL; } exitcs();////// Critical Section End Listagem 3.5: Construtor da classe Mutex No que diz respeito ao método take este deve ser chamado por uma tarefa antes de aceder a um recurso partilhado. Por outras palavras, este método garante à tarefa exclusividade sobre o recurso. Se o mutex já estiver associado a uma tarefa (sinalizador binário ativado), a outra tarefa que o invocou será suspensa até que o mutex seja libertado. É possível que várias tarefas estejam em espera do mesmo mutex, todavia uma vez que a lista de espera é ordenada pela prioridade das tarefas, assim que o mutex é libertado apenas a tarefa de maior prioridade é acordada. Relativamente ao método release, embora este possa ser invocado por qualquer tarefa, é expectável que apenas o invoque a tarefa que anteriormente tenha chamado o método take. Isto significa que apenas faz sentido que a tarefa que sinalizou o acesso a um recurso seja a mesma a libertar esse recurso. Um possível resultado de libertar o mutex pode ser o de acordar uma tarefa de maior prioridade. Nesse caso, a tarefa que libertou o recurso deve ser forçada a ceder a execução à tarefa de maior prioridade que estava à espera desse mesmo recurso. 46

65 Capítulo 3. Especificação do Sistema 3.3 Template MetaProgramming Descoberta a possibilidade de aplicação em 1994 por Erwin Unruh, e aplicada em 1998 por Krzysztof Czarnecki [6], o template metaprograming é uma técnica que utiliza templates para gerar e manipular o código de uma aplicação em tempo de compilação (compile time) [45]. Assim, com a utilização desta técnica é possível expandir as capacidades do compilador, permitindo que atue momentaneamente como um interpretador, de forma a produzir configurações estáticas e otimizadas. A sintaxe e idiomas do TMP são isotéricos quando comparados com a programação convencional em C++. Por outras palavras, o código TMP (código estático C++) é considerávelmente diferente, e mais difícil de perceber, que o código C++ standard (código dinâmico C++). O código C++ dinâmico é imperativo e orientado a objetos, enquanto o código C++ estático pode mesmo ser considerado funcional. Como o TMP pode ser considerado uma linguagem de programação funcional, este não possui variáveis, atribuições, e iterações. O código é baseado no conceito de funções matemáticas, onde cada passo do processo é separado em múltiplos casos, e, normalmente, utiliza as funções recursivamente Blocos Básicos do Template Metaprogramming O código C++ TMP é composto essencialmente por quatro blocos básicos: (i) valores; (ii) funções; (iii) saltos condicionais; e (iv) recursividade. [55] Em TMP as variáveis não podem ser modificadas, uma vez que são nomes prédefinidos (typedefed) e constantes. Caso seja requerido um novo tipo ou valor, este deve ser implementado dessa forma. O código da listagem 3.6 mostra como se faz essa definição. // named value definition struct NamedValue typedef int value; } ; // integer value definition struct IntegerValue enum value = 2 } ; } ;... // using named and integer values 47

66 3.3. Template MetaProgramming NamedValue::value var = 19; int x = IntegerValue::value; Listagem 3.6: Valores em template metaprogramming As funções, ou mais precisamente metafunções, são definidas em TMP utilizando estruturas ou classes. Para passar meta-argumentos às metafunções são utilizados argumentos template. Para definir o valor ou tipo de retorno são utilizados nomes pré-definidos ou valores inteiros. A listagem 3.7 apresenta um exemplo de uma metafunção para a adição de dois inteiros. // function definition template<int X, int Y> struct Add // define the result type typedef int result type; // store the result value enum result = X + Y } ; } ;... // call Add function Add::result type var = Add<2,3>::result; Listagem 3.7: Funções em template metaprogramming Sempre que sejam necessários utilizar construtores condicionais, são usadas as templates especializadas. Em compile time o compilador instância a template que melhor se identifica com os meta-argumentos especificados. O código da listagem 3.8 implementa a especialização de templates para verificar se dois tipos são idênticos (is same). // generic implementation template<typename T, typename U> struct is same enum result = 0 } ; } ; // partial specialized implementation template<typename T> struct is same<t, T> enum result = 1 } ; } ;... 48

67 Capítulo 3. Especificação do Sistema // check if the provided types are the same bool value = is same<int, char>::result; Listagem 3.8: Saltos condicionais em template metaprogramming Tal como nas linguagens funcionais, também o código TMP utiliza recursividade em vez da iteração (ciclos). Para parar a recursão, é definida uma template especializada. A listagem 3.9 implementa uma metafunção para o cálculo da soma dos n primeiros números inteiros. // generic implementation template <unsigned n> struct sum enum value = n + sum<n 1>::value } ; }; // stop condition template <> struct sum<0> enum value = 0 } ; };... // call sum metafunction int result = sum<4>::value; Listagem 3.9: Recursividade em template metaprogramming O Fatorial Um exemplo básico para demonstrar as potencialidades do C++ template metaprogramming consiste no cálculo do fatorial de um número. A implementação standard (dinâmica) para o cálculo do fatorial, consiste na implementação de uma função iterativa ou recursiva, que é invocada durante a execução da aplicação. O código da listagem 3.10 apresenta a implementação recursiva em linguagem C++. // dinamic factorial function int factorial(int n) if(n == 0) return 1; } return n factorial(n 1); } 49

68 3.3. Template MetaProgramming... // call factorial function int value = factorial(3); Listagem 3.10: Implementação C++ recursiva do cálculo do fatorial Com esta implementação, o resultado do fatorial do número três é conhecido em tempo de execução. No entanto, em tempo de compilação, o número para o qual se pretende calcular o fatorial já é conhecido. Assim sendo, utilizando C++ TMP, é possível calcular em compile time o resultado da constante correspondente ao fatorial de três. O código apresentado na listagem 3.11 traduz a implementação estática em TMP do cálculo do fatorial desse número. // generic implementation template<int n> struct Factorial enum value = Factorial<n 1>::value n}; }; // specific implementation/stop condition template<> struct Factorial<0> enum value = 1}; };... // call factorial metafunction int value = Factorial<3>::value; Listagem 3.11: Implementação C++ TMP recursiva do cálculo do fatorial De forma sucinta, o primeiro trecho de código implementa a template genérica do cálculo do fatorial, enquanto o segundo implementa a template especializada para a condição de paragem da recursão. A figura 3.6 ilustra o processo que o compilador utiliza para resolver os templates no cálculo do fatorial. Para ter uma ideia do nível de optimização do código gerado com a utilização do TMP, o autor decidiu avaliar, nesta fase preliminar, o desempenho e os recursos de memória de cada uma das aplicações (estática e dinâmica), implementadas no microcontrolador O desempenho da aplicação foi obtido utilizando o debugger do ambiente de desenvolvimento, enquanto a memória de código (sem otmizações do compilador) foi conseguida com a utilização do FLIP da Atmel [56]. A tabela 3.3 apresenta os resultados obtidos. 50

69 Capítulo 3. Especificação do Sistema Figura 3.6: Resolução dos templates no cálculo do fatorial Tabela 3.3: Resultados de desempenho e memória das aplicações Fatorial (C++ dinâmico) e Fatorial (TMP) Aplicação Tempo execução (ciclos relógio) Memória de código (bytes) Fatorial (C++ dinâmico) Fatorial (TMP) Lista Ligada Estática Um exemplo mais avançado que ilustra a aplicabilidade do TMP consiste na implementação estática de uma lista ligada (linked list). Uma lista ligada é uma estrutura de dados que consiste num grupo de nós, que globalmente representam uma sequência. De forma simplificada, cada nó é composto por dados e uma referência (link) para o próximo nodo da sequência. A implementação estática da lista ligada é semelhante à lista ligada dinâmica, no entanto tudo é resolvido em compile time, reduzindo o tempo de execução de uma determinada tarefa, e aumentando portanto o desempenho do sistema. Por exemplo, supondo que se pretende determinar o número de ocorrências da letra a num ficheiro de texto, a ideia passa por implementar uma lista ligada estática em que cada nodo da lista é preenchida com um caracter do ficheiro de texto. Depois disso, basta percorrer a lista ligada e incrementar um contador a cada ocorrência do caracter a. O código da listagem 3.12 apresenta a implementação de uma lista ligada estática de inteiros. const int endvalue = ( 0u >> 1); //lowest integer value //Linked List Implementation struct End 51

70 3.4. Ambiente de Desenvolvimento enum head = endvalue}; typedef End Tail; }; template<int head, typename Tail = End> struct Cons enum head = head }; typedef Tail Tail; };... //Create a Linked List Cons<1, Cons<2, Cons<3, End> > >; Listagem 3.12: Implementação C++ TMP de uma lista ligada estática de inteiros Com esta lista é possível implementar metafunções para determinar, por exemplo, o tamanho (length), ou então se está vazia (is empty). A listagem 3.13 apresenta a metafunção Lenght. A metafunção utiliza recursividade, implementando portanto a template genérica e a template específica para a condição de paragem. // LL Length Implementation template<typename List> struct Lenght enum value = Lenght<typename List::Tail>::value + 1 }; }; template<> struct Lenght<End> enum value = 0 }; }; Listagem 3.13: Metafunção Length da lista ligada estática Resumindo, em tempo de compilação é possível definir a lista ligada, assim como utilizar as metafunções para determinar algumas das suas características. Mais uma vez, só para mostrar o poder de otimização das implementações com TMP, é apresentado na tabela 3.4 uma pequena aplicação em C++ com TMP e o respectivo código assembly gerado pelo compilador para a arquitetura Ambiente de Desenvolvimento Nos sistemas informáticos de propósito geral, assim como nos sistemas embebidos, para converter o código fonte de uma aplicação, escrito numa linguagem de 52

71 Capítulo 3. Especificação do Sistema Tabela 3.4: Código C++ TMP e código assembly da aplicação estática do fatorial Código C++ com TMP void main () typedef Cons<1,Cons<2,Cons<3,End>>> list1; P0 = Lenght<list1>::value; P0 = IsEmpty<list1>::value; } Código assembly main: CODE ; Auto size: 0 ; P0 = list1.lenght (3) MOV 0x80,#0x3 ; P0 = list1.isempty (1) MOV 0x80,#0x1 RET programação de alto nível, para código objeto ou mesmo código máquina, é necessário recorrer sobretudo a três ferramentas: (i) compilador, (ii) assembler e (iii) linker. Os compiladores podem ser definidos como programas para computador que traduzem uma linguagem para outra [57]. Por outras palavras, um compilador recebe como entrada o código fonte de uma determinada aplicação, e produz como saída um programa semanticamente equivalente, porém escrito noutra linguagem. Geralmente, o código fonte é escrito numa linguagem de alto nível, como C ou C++, e é convertido para código objeto específico ao processador. Por sua vez, um assembler traduz o código em linguagem assembly para código objeto ou código máquina próprio do processador [57] (3.7a). A linguagem assembly é uma forma simbólica da linguagem máquina dos processadores e é particularmente fácil de traduzir. Às vezes, alguns compiladores geram mesmo código assembly como saída, e de seguida chamam o assembler para concluir a tradução em código objeto (3.7b). Tanto os compiladores como os assemblers muitas vezes dependem de um programa chamado linker. Esta ferramenta é então responsável pela fusão de todo o código relocatable (código que tem símbolos por resolver, que o compilador não reconhece porque compila os ficheiros separadamente) presente nos ficheiros objetos, num único ficheiro executável [57]. Tal como foi referido na secção 3.2, o sistema operativo ADEOS foi desenhado segundo o paradigma da orientação a objetos, sendo portanto implementado com uma linguagem de programação orientada a objetos, concretamente C++. Além disso, determinadas rotinas críticas do sistema operativo estão implementadas em linguagem assembly. Neste sentido, para traduzir esse código fonte escrito em C++ para código assembly ou código objecto, é necessário um compilador C++ para o processador alvo, ou seja, um compilador C++ para o Mais, é também necessário um assembler e um linker para o 8051, de modo a converter o código assembly das rotinas críticas em código objeto, e fundir todo o código objecto e traduzir em código 53

72 3.4. Ambiente de Desenvolvimento (a) (b) Figura 3.7: Processo de compilação de código fonte em código executável/máquina máquina específico ao processador, respectivamente. Com efeito, o autor investigou quais os ambientes de desenvolvimento disponíveis no mercado que integrassem as ferramentas especificadas anteriormente. As soluções encontradas foram unicamente duas: (i) Ceibo 8051 C++ Compiler + Keil uvision IDE [58] e (ii) IAR Embedded Workbench for 8051 [59]. Relativamente à primeira, consiste na integração do compilador C++ da Ceibo com o software Keil, permitindo assim a compilação de código C++, C e assembly em código objeto. Esse código objeto é depois traduzido em código máquina com o linker do Keil. O editor e o debugger também fazem parte do IDE Keil. Portanto, esta solução consiste numa dualidade de esforços por parte da Ceibo e da Keil Software. Por outro lado, a segunda 54

73 Capítulo 3. Especificação do Sistema solução consiste na utilização da Embedded Workbench para o microcontrolador 8051 desenvolvida pela IAR. Este ambiente de desenvolvimento integra conjuntamente não só compilador C/C++, assemblador e linker, assim como editor e debugger. Portanto, todas as ferramentas são desenvolvidas por uma única entidade, a IAR SYSTEMS. Analisando e comparando as soluções, o autor decidiu optar pela IAR Embedded Workbench pelas seguintes razões: O compilador da Ceibo não é actualizado desde 2002, e requer a versão do Keil uvision2 (atualmente o software Keil encontra-se na versão uvision4 ). O software da IAR foi atualizado em Fevereiro do presente ano; O Compilador C++ da Ceibo não suporta templates, o que impossibilita a aplicação de C++ TMP para a gestão da variabilidade do SO, essencial para o sucesso deste trabalho. O compilador da IAR na versão IAR Extended Embedded C++ (EEC++) suporta; Compilador IAR C/C++ para o 8051 O IAR C/C++ Compiler for 8051 é uma das ferramentas integradas na IAR Embedded Workbench for Este programa permite a compilação de duas linguagens de programação de alto-nível: C, a linguagem de programação mais usada na indústria dos sistemas embebidos. É possível desenvolver aplicações que sigam os standards: Standard C : também conhecido como C99; C89: também conhecido como C94, C90, C89 e ANSI C. C++, a linguagem de programação orientada a objetos, com bibliotecas com recursos para a programação modular. Qualquer um dos seguintes standards pode ser usado: Embedded C++ (EC++): um subconjunto de funcionalidades da programação standard C++, definidas pelo consorcio Embedded C++ Technical committee; IAR Extended Embedded C++ (EEC++): corresponde ao EC++ com funcionalidades adicionais, como suporte completo a templates, namespace e Standard Template Library (STL). 55

74 3.4. Ambiente de Desenvolvimento Memória de Código Conforme foi explicado da secção 3.1, no 8051 clássico o tamanho da memória de código é de 4k-byte com possibilidade de extensão até 64k-byte. Por sua vez, existem alguns 8051/8052 em que a memória de código é expandida através do conceito de bancos. É possível estender a memória até 16M-byte utilizando 256 bancos de 64kbyte. O C8051F12X da Silabs [60] e o CC2430 da Texas Instruments [61] são alguns exemplos onde isso é feito por hardware. Mas, além disso, existem ainda dispositivos com memória de código estendida, o que significa que podem ter até 16M-byte de memória de código linear. Os dispositivos da Maxim DS80C390/DS80C400[62, 63] são exemplo disso. O compilador da IAR suporta todas as configurações da memória de código apresentadas acima. Para especificar o núcleo e o modelo de memória de código pretendido este pode ser feito de duas formas: No IAR Embedded Workbench IDE, escolhendo Project->Options->General Options->Target->CPUcore e Project->Options->General Options- >Target->Codemodel; Através da linha de comandos com a opção de compilação core = plain p1 extended1 e1 extended2 e2 } e code model = near n banked b banked ext2 b2 far f } ; Memória de Dados Relativamente ao modelo de dados, ou seja, ao modelo que especifica o tipo de memória usada por defeito para armazenar os dados, o compilador da IAR suporta seis, dos quais importa destacar os seguintes: Tiny - O modelo de dados Tiny usa a memória tiny por defeito, que está localizada nos primeiros 128-byte do espaço de memória de dados interna. Esta memória pode ser acedida usando endereçamento directo. A vantagem é que são apenas necessários 8-bit para o apontador. Small - O modelo de dados Small usa, por defeito, os primeiros 256-byte do espaço de memória de dados interna. Esta memória pode ser acedida com apontadores de 8-bits, tendo então como vantagem ser apenas necessários 8-bit para o apontador. 56

75 Capítulo 3. Especificação do Sistema Large - O modelo de dados Large usa, por defeito, os primeiros 64k-kbyte do espaço de memória de dados externa. Esta memória pode ser acedida apenas com apontadores de 16-bit. Para especificar o modelo de dados no compilador, é possível fazê-lo de duas formas: No IAR Embedded Workbench IDE, escolhendo Project -> Options -> GeneralOptions -> Target -> Data model; Através da linha de comandos com a opção de compilação data model = tiny t small s large l far f far generic fg generic g } ; Funções Para além do tradicional suporte a funções standard C, este compilador fornece um conjunto de extensões - mecanismos que controlam as funções - que permitem acrescentar e personalizar determinados aspetos inerentes às mesmas. Desta forma, seja através das opções de compilação, da utilização de keywords ou diretivas pragma, ou mesmo com o uso de funções intrínsecas, é possível controlar onde é que as funções são armazenadas em memória, usar primitivas para programar interrupções e concorrência, configurar e utilizar o sistema de bancos do microcontrolador 8051, otimizar funções, e aceder a recursos de hardware. Por exemplo, configurando o modelo de código (near ou banked) é possível controlar o espaço de memória para o armazenamento das funções, nomeadamente o tamanho máximo e o conjunto de endereços dedicados. Para definir uma função interrupção, tem que ser usada a keyword interrupt e a directiva pragma vector. Com a directiva especifica-se qual a interrupção pretendida do vector de interrupções existente no microcontrolador, e com a keyword define-se que a função é uma rotina de serviço à interrupção. O código da listagem 3.14 mostra como definir uma função interrupção para o overflow do temporizador 0 do Uma função do tipo interrupção, obrigatoriamente, não pode retornar nada (tipo de retorno void), e não pode especificar nenhum parâmetro. #pragma vector = TF0 int / Symbol defined in I/O header file / interrupt void MyISR(void) / ISR code / 57

76 3.4. Ambiente de Desenvolvimento } Listagem 3.14: Função de interrupção de overflow do timer 0 Interface Assembly Quando se desenvolvem aplicações, sobretudo para sistemas embebidos, é normal existirem situações onde é necessário escrever partes de código em linguagem assembly. Seja para obter timings precisos, seja para escrever sequencias especiais de instruções, para obter melhorias a nível de performance, ou então simplesmente porque os compiladores mesmo com recurso aos vários pragmas não conseguem aceder a todos os recursos de hardware. Conforme foi visto na secção 3.2, o sistema operativo ADEOS não é exceção, e tanto a rotina de inicialização de contexto (contextinit) como de mudança de contexto (contextswitch) estão escritas em assembly. Desta forma, para se poder fazer o porting do sistema operativo para a plataforma MCS- 51 é preciso perceber de que forma é que o compilador IAR para o 8051 suporta o interface com o assembly. Assim sendo, o compilador IAR C/C++ para o 8051 disponibiliza três formas de aceder aos recursos de baixo nível: (i) assembly inline; (ii) módulos escritos inteiramente em assembly; e (iii) funções intrínsecas. Relativamente à primeira, é possível inserir código assembly diretamente em funções escritas em C e C++, através da utilização da keyword asm. O código apresentado na listagem 3.15 é um pequeno exemplo da utilização do inline assembler para introduzir instruções assembly num pequeno programa em C. É possível introduzir apenas uma instrução, ou então um bloco de instruções. É importante não esquecer que as instruções inline são inseridas naquela localização no programa. Portanto, é preciso ter presente as possíveis consequências da indevida utilização da mesma. int main() int a = 2; asm( MOV SP,#0x80 ); //change stack adress int b = 0; asm( PUSH 0 \n\t MOV A,#10 \n\t MOV 0,A \n\t POP 0 \n\t ); return 0; 58

77 Capítulo 3. Especificação do Sistema } Listagem 3.15: Exemplo de utilização de inline assembler no compilador IAR No que diz respeito à segunda possibilidade, o compilador permite chamar rotinas escritas totalmente em assembly (em ficheiros assembler) a partir do C ou C++. Como o trabalho do autor está enquadrado na programação orientada a objetos, será somente explicado o método para C++, podendo o leitor consultar mais detalhes para linguagem C no Manual do compilador IAR C/C++ para o 8051 [64]. Desta forma, em primeiro lugar é preciso declarar o nome, parâmetros e retorno da função no ficheiro de código C++, conforme é apresentado na listagem extern C int assembler routine(int val); } Listagem 3.16: Definição de uma função implementada num ficheiro assembly externo Depois, no ficheiro assembler, as rotinas devem ser declaradas como públicas e deve ser especificado o código de cada uma delas. O ficheiro assembly deve ser estruturado conforme apresentado na listagem Os parâmetros das funções são passados através dos registos R0-R5 ou pela pilha, dependendo no número e tipo de parâmetros em questão. O retorno é somente feito através dos registos R0-R5. Na subsecção seguinte será analisado e explicado com mais detalhe a convenção de chamada suportada pelo compilador. NAME assembler example RSEG DOVERLAY:DATA:NOROOT(0) RSEG IOVERLAY:IDATA:NOROOT(0) RSEG ISTACK:IDATA:NOROOT(0) RSEG PSTACK:XDATA:NOROOT(0) RSEG XSTACK:XDATA:NOROOT(0) ;Name of Assembler functions here PUBLIC assembler routine RSEG NEAR CODE:CODE:NOROOT(0) ;Declaration of functions here assembler routine: ;Assembly Code END Listagem 3.17: Estrutura de um ficheiro assembly gerado pelo compilador IAR Finalmente, a terceira e última forma de interface assembly consiste na utilização de funções intrínsecas, isto é, são funções pré-definidas disponibilizadas pelo compilador que permitem aceder aos recursos de baixo nível sem ter de usar a linguagem 59

78 3.4. Ambiente de Desenvolvimento Tabela 3.5: Convenções de chamada de funções no compilador C/C da IAR Convenção de chamada Data overlay Idata overlay Idata reentrant Pdata reentrant Xdata reentrant Extended stack reentrant Atributo da função Stack pointer Descrição data overlay Uma porção da memória interna com acesso direto é usada para dados e parâmetros idata overlay Uma porção da memória interna com acesso indireto é usada para dados e parâmetros idata reentrant SP A pilha da memória interna com acesso indireto (idata) é usada para dados e parâmetros pdata reentrant PSP Uma pilha emulada na (pdata) é usada para dados e parâmteros xdata reentrant XSP Uma pilha emulada na (xdata) é usada para dados e parâmetros ext stack reentrant ESP:SP Uma pilha estendida é usada para dados e parâmetros assembly. A vantagem das funções intrínsecas relativamente ao uso de inline assembler, é que o compilador tem toda a informação necessária para garantir uma correta sequência de interface, isto é, garante que tanto os registos como as variáveis são corretamente salvaguardados e restaurados. Convenção de Chamada de Funções Normalmente, as funções podem ser invocadas dentro de um programa por nome ou por endereço. A convenção de chamada é o processo subjacente a essa invocação gerida automática e transparentemente pelo compilador, delegando responsabilidades à função chamada e ao chamante. Contudo, se uma função for escrita em linguagem assembly, é necessário saber onde e como os parâmetros podem ser encontrados, bem como quando retornar ao chamante e como retornar o resultado. O compilador IAR 60

79 Capítulo 3. Especificação do Sistema Tabela 3.6: Registos utilizados nos parâmetros das funções Parâmetro 1-bit 8-bit 16-bit 32-bit Passado nos registos B.0, B.1, B.2, B.3, B.4, B.5, B.6, B.7, VB.0, VB.1, VB.2, VB.3, VB.4, VB.5, VB.6 ou VB.7 R1, R2, R3, R4 ou R5 R3:R2 ou R5:R4 R5:R4:R3:R2 C/C++ para o 8051 suporta seis diferentes convenções de chamada, responsáveis por controlar como é que a memória é usada para os parâmetros e as variáveis locais. A tabela 3.5 lista as diversas convenções de chamada disponíveis. Para especificar a convenção de chamada utilizado por defeito pelo compilador, é possível fazê-lo de duas formas: No IAR Embedded Workbench IDE, escolhendo Project -> Options -> GeneralOptions -> Target -> Calling model ; Através da linha de comandos com a opção de compilação calling convention = data overlay do idata overlay io idata reentrant ir pdata reentrant pr xdata reentrant xr ext stack reentrant er } ; Apesar de apenas ser possível definir uma convenção de chamada para cada projeto em cada instante de tempo, o compilador possibilita definir a convenção de chamada para funções individuais através da utilização dos atributos apresentados na tabela 3.5. Prólogo da função Os parâmetros podem ser passados para uma função usando três métodos distintos: em registos, na pilha, em janelas de memória (overlay frame). É muito mais eficiente usar os registos do que utilizar a pilha, daí que todas as convenções de chamada tenham sido desenhadas para maximizar o uso de registos. Apenas um número limitado de registos pode ser usado para a passagem de parâmetros. A tabela 3.6 apresenta os registos que podem ser utilizados para a passagem de parâmetros. Quando não estejam disponíveis mais registos, os restantes parâmetros são passados pela pilha. Em alguns casos, nomeadamente em estruturas, uniões, classes ou parâmetros de funções com tamanho variável (ellipsis), estes são sempre passados 61

80 3.4. Ambiente de Desenvolvimento Tabela 3.7: Registos utilizados no retorno das funções Valores de Retorno 1-bit 8-bit 16-bit 32-bit Passado nos registos Carry (C) R1 R3:R2 R5:R4:R3:R2 pela pilha. Os parâmetros passados por pilha são guardados na memória na localização apontada pelo apontador da pilha especificada pela convenção da chamada. O primeiro parâmetro é colocado diretamente na localização seguinte ao endereço apontado pelo apontador da pilha. A pilha da convenção idata e extended stack cresce para endereços de memória superiores, enquanto a pilha da convenção xdata e pdata cresce para endereços de memória inferiores. Epílogo da função Uma função pode ou não retornar um valor para o chamante. O retorno de uma função, se existir, pode ser escalar (inteiro ou apontador), ponto-flutuante, ou estrutura. Em todas as convenções de chamada, o valor de retorno é passado em registos ou no bit de carry. A tabela 3.7 apresenta os registos que podem ser utilizados para o valor de retorno das funções. Ambiente de Execução - DLIB O ambiente de execução corresponde ao ambiente na qual a aplicação é executada. Este depende do hardware alvo, do ambiente de software, e do código da aplicação, e disponibiliza: Suporte às características do hardware, nomeadamente acesso direto à camada de baixo nível do processador (funções intrínsecas), registos dos periféricos e interrupções (ficheiros cabeçalho); Suporte a ambiente de execução, isto é, código para a inicialização e término do sistema; Suporte a operações de ponto-flutuante (fenv); O compilador IAR C/C++ para o 8051 possibilita a execução de aplicações em dois ambientes de execução: (i) CLIB; e (ii) DLIB. Enquanto o primeiro apenas 62

81 Capítulo 3. Especificação do Sistema pode ser utilizado com linguagem C, o segundo suporta tanto C como C++. Assim sendo, no contexto do trabalho a desenvolver, interessa apenas ao autor perceber o ambiente de execução DLIB. Portanto, este consiste numa biblioteca de execução, que contém funções definidas em C e C++, e ficheiros cabeçalho que definem a interface da biblioteca (headers). Essa biblioteca de execução é disponibilizada tanto sob a forma de bibliotecas pré-compiladas ($IAR directory/8051/lib/dlib) como ficheiros de código fonte ($IAR directory/8051/src/lib/dlib). As bibliotecas pré-compiladas são configuradas para diferentes combinações das seguintes características: ambiente de execução DLIB; variante do core; localização da pilha; modelo de código; modelo de dados; convenção de chamada; localização das constantes; e número, visibilidade, tamanho e método de seleção do(s) data pointer(s). O nome da biblioteca pré-compilada é gerado com a seguinte configuração: lib} - core} stack} - code mod} data mod} cc} const loc} - dptrs} dptr vis} dptr size} dptr select}.r51. Caso o compilador não disponibilize uma biblioteca DLIB pré-compilada para as combinações pretendidas, ou então caso seja necessário alterar as rotinas de startup ou exit, ou caso seja mesmo necessário adicionar suporte a alguma funcionalidade, é possível criar uma biblioteca customizada. O processo é complexo, e toda a informação pode ser consultada no manual do compilador. Finalmente, para terminar, importa referir que a biblioteca DLIB não pode ser construída para os modelos de dados Tiny e Small, devido a necessidade de certos recursos inerentes à linguagem C++. 63

82

83 Capítulo 4 Implementação do Sistema Este capítulo descreve o desenvolvimento dos componentes do sistema especificados no capítulo anterior. Basicamente, o capítulo anterior permitiu a familiarização com a arquitetura do microcontrolador alvo, o sistema operativo orientado a objetos, a técnica de programação para a gestão da variabilidade, bem como o compilador C++ a utilizar. Este capítulo descreve então o trabalho concretamente desenvolvido. Numa primeira fase é explicado o processo de porting do ADEOS, ou seja, é analisado o código dependente do microcontrolador 80188, e apresentada a implementação para o De seguida, na fase de upgrade, são explicadas as melhorias introduzidas no ADEOS. Clock-tick intrínseco ao escalonador, device-drivers para os diversos periféricos, e escalonador power-aware. Finalmente, no final do capítulo é apresentado e explicado o refactoring do sistema operativo com template metaprogramming, de modo a permitir e possibilitar a sua customização de acordo com as necessidades do utilizador. 4.1 Porting do ADEOS para a Plataforma MCS- 51 Todo o código de software pode ser classificado, segundo o conceito de portabilidade, de duas formas distintas: (i) código dependente do processador (CDP); e (ii) código independente do processador (CIP). Portanto, ou estamos perante código universal que corre em qualquer plataforma, como bytecode compilado em Java para máquinas virtuais, ou então código binário que corre apenas numa arquitetura dedi- 65

84 4.1. Porting do ADEOS para a Plataforma MCS-51 cada. Regra geral, quanto mais próxima a linguagem de programação for do hardware, menos portável esta é. Assim sendo, o porting de software consiste basicamente em reescrever o CDP de uma arquitetura original, para outra arquitetura alvo. Neste sentido, para efetuar o porting do sistema operativo ADEOS da arquitetura para a arquitetura 8051, basta alterar, reescrever e adaptar o código BSP (escrito em assembly específico ao 80188). Analisando a figura 4.1, que ilustra a arquitetura de software do ADEOS e a sua relação com o hardware, é possível constatar que para efetuar o porting deste sistema operativo, basta portanto alterar e reescrever o código dos ficheiros bsp.h e bsp.asm. Basicamente, esses ficheiros contém o código responsável por inicializar o contexto das tarefas, assim como realizar a mudança de contexto entre as mesmas. Figura 4.1: Arquitetura de software do ADEOS O autor decidiu, de modo a tornar a tarefa mais organizada e simplificada, dividir a actividade de porting do SO em duas fases subsequentes: (i) analisar e compreender o código assembly específico ao 80188; (ii) substituir o código assembly pelo código assembly 8051, procurando manter a estrutura e estratégia (o mais fidedigno quanto possível) de inicialização e mudança de contexto utilizada no processador original; 66

85 Capítulo 4. Implementação do Sistema Análise do Código Dependente do Processador A primeira tarefa de porting do ADEOS para a arquitetura 8051 passa então por analisar e perceber o código, específico ao 80188, responsável pela inicialização e mudança de contexto das tarefas do SO. Esta tarefa torna-se essencial para o autor, não só para interiorizar e assimilar conceitos inerentes ao porting de software, assim como perceber a estratégia e abordagem utilizada pelo projetista do sistema operativo, para relacionar e perceber os contornos da mudança para a arquitetura Além disso, vai permitir que o mesmo adquira competência e conhecimentos relativamente à arquitetura e conjunto de instruções do Ficheiro Cabeçalho (bsp.h) O ficheiro cabeçalho bsp.h (listagem 4.1) é utilizado para definir a estrutura de dados responsável por guardar o estado da máquina de cada tarefa (contexto), especificar as macros que delimitam secções de código crítico, e declarar o protótipo das funções implementadas em assembly responsáveis pela inicialização e mudança de contexto. Além disso, é ainda gerido o problema de name mangling subjacente ao interface entre as linguagens C e C++. struct Context int IP; int CS; int Flags; int SP; int SS; int SI; int DS; }; #include task.h #define entercs() asm pushf; cli } #define exitcs() asm popf } extern C void contextinit(context, void ( run)(task ), Task, int pstacktop); void contextswitch(context poldcontext, Context pnewcontext); void idle(); }; Listagem 4.1: Ficheiro bsp.h para a arquitetura A estrutura Context permite guardar o estado atual do processador, isto é, o valor dos registos essenciais do utilizados por uma determinada tarefa. Neste 67

86 4.1. Porting do ADEOS para a Plataforma MCS-51 caso, os registos necessários a salvaguardar são: o Instruction Pointer (IP); o Code Segment (CS); as flags (Flags); o Stack Pointer (SP); o Stack Segment (SS); o Source Index (SI); e o Data Segment (DS). As macros entercs e exitcs permitem delimitar secções de código consideradas críticas. Por outras palavras, sempre que uma porção de código não possa ser interrompido, então este é considerado uma secção de código crítica, não podendo as interrupções estarem habilitadas. Daí que as macros sejam implementadas em inline assembler, com recurso às instruções pushf, cli, e popf. Segundo o conjunto de instruções do 80x86 [65] (compatível com o 80188), a instrução pushf guarda as flags na pilha, a instrução cli desabilita a flag de interrupção, e a instrução popf restaura as flags da pilha. Finalmente, a utilização da diretiva extern "C", serve para informar o compilador que as funções foram escritas em assembly seguem a convenção de nomes do C, que é diferente da convenção de nomes do C++. Ficheiro Assembly (bsp.asm) O ficheiro assembly bsp.asm contém a implementação das três funções declaradas no ficheiro cabeçalho bsp.h, ou seja, implementa a função de inicialização do contexto, a função de mudança de contexto, e a função idle. Inicialização de Contexto Relativamente à função de inicialização do contexto - contextinit -, esta apresenta uma estratégia de implementação baseada em cinco etapas. O algoritmo 1 apresenta a estratégia utilizada. Antes de explicar propriamente a implementação da função, convém perceber o protótipo da mesma (listagem 4.2). Assim, a função contextinit tem quatro parâmetros de entrada. O primeiro é um apontador para a estrutura do contexto da tarefa, o segundo um apontador para a rotina de startup da tarefa, o terceiro um apontador para o objeto da tarefa, e o quarto e último parâmetro um apontador para o endereço do topo da pilha dedicada à tarefa. A função não retorna nenhum valor (void). void contextinit(context, void ( run)(task ), Task, int pstacktop); Listagem 4.2: Protótipo da função contextinit A primeira etapa da inicialização do contexto representa a primeira parte do prólogo da função. Resume-se em gravar o base pointer na pilha do sistema, actualizar 68

87 Capítulo 4. Implementação do Sistema Algoritmo 1 Inicialização do contexto no contextinit contextinit(...): aceder ao apontador da estrutura context da tarefa; inicializar o endereço de retorno; inicializar as flags do processador; inicializar o segmento da stack; inicializar o segmento de dados; esse base pointer depois de o ter gravado, e posteriormente, através da instrução les, obter o apontador para a estrutura do contexto passado como parâmetro pela pilha, colocando 16-bit do endereço no destination index e os outros 16-bit no extra segment. O código apresentado abaixo representa a implementação, e a imagem 4.2 ilustra a organização da pilha do sistema logo após a chamada da função e execução destas instruções. push bp mov bp, sp les di, dword ptr ss:[bp+6]; Get pcontext. Figura 4.2: Pilha do sistema após entrada na função contextinit 69

88 4.1. Porting do ADEOS para a Plataforma MCS-51 Na segunda etapa (continuação do prólogo e início do corpo da função) iniciase o preenchimento da estrutura de dados do contexto da tarefa, concretamente é inicializado o endereço de retorno de startup da tarefa. push ds lds bx, dword ptr ss:[bp+10]; Get pfunc from the caller. mov dx, ds mov es:[di], bx mov es:[di+2], dx Basicamente, com a instrução lds obtém-se o apontador da rotina de startup passado como parâmetro (16-bit do endereço no registo base e os outros 16-bit no data segment), e com as duas últimas duas instruções mov preenche-se o primeiro (IP) e segundo elemento (CS) da estrutura do contexto da tarefa (es:[di]) com o endereço do apontador pfunc. A terceira etapa é responsável por inicializar as flags do processador na estrutura do contexto da tarefa. pushf pop ax or ax, b; Enable interrupts by default. mov es:[di+4], ax Para isso, começa por guardar as flags na pilha (pushf), restaura as flags para ao acumulador (pop ax) e activa as interrupções por defeito. Por fim, com a instrução mov preenche o terceiro elemento(flags - es:[di+4]) da estrutura com esse valor. A quarta etapa é a etapa mais complexa da rotina de inicialização de contexto, pois inicializa-se a área de memória reservada à pilha da tarefa. les di, dword ptr ss:[bp+18]; Point to the task s stack. lds bx, dword ptr ss:[bp+14]; Get ptask from the caller. mov dx, ds mov es:[di 4], bx ; Place ptask onto the stack. mov es:[di 2], dx les di, dword ptr ss:[bp+6] ; Point to the task s context. lds bx, dword ptr ss:[bp+18]; Get pstack from the caller. mov dx, ds sub bx, 8 ; Save stack space for ptask. mov es:[di+6], bx mov es:[di+8], dx Assim sendo, as duas primeiras instruções assembly permitem obter, respectivamente, o apontador para o topo da pilha da tarefa (endereços em es e di) e o apontador para o objeto da tarefa (endereços em ds e bx). Depois disso, guardase o endereço do objeto da tarefa (endereços em bx e dx) nos primeiros endereços da própria pilha reservada para a tarefa (es:[di-4] e es:[di-2]). As duas instruções 70

89 Capítulo 4. Implementação do Sistema seguintes permitem aceder, respectivamente, ao apontador para a estrutura do contexto da tarefa (endereços em es e di) e novamente o apontador para o topo da pilha da tarefa (endereços em ds e bx). A instrução sub subtrai 8 unidades ao endereço do topo da pilha da tarefa, e as duas instruções seguintes preenchem o quarto (SP - es:[di+6]) e quinto elemento (SP - es:[di+8]) da estrutura com os endereços da pilha da tarefa atualizada. A imagem 4.3 representa a organização da pilha da tarefa após a execução desse bloco de código. Figura 4.3: Pilha da tarefa após inicialização A quinta e última etapa inicializa o segmento de dados, isto é, preenche na estrutura do contexto da tarefa os valores dos registos de segmentos si e ds. Além disso, é também responsável por implementar o código epílogo da função (instruções pop e ret). pop ds mov dx, ds mov es:[di+10], si mov es:[di+12], dx pop bp ret Neste sentido, na sequência da instrução push ds da segunda etapa, que continha o valor inicial desse registo, a instrução pop ds restaura então novamente o registo. Desta forma, as instruções seguintes preenchem o sexto (SI - es:[di+10]) e sétimo elemento (DS - es:[di+12]) da estrutura com o valor original desses registos. A instrução pop bp restaura o base pointer com o valor que este tinha antes da chamada da função. A etapa termina com a instrução ret, responsável por retornar a execução de código para a instrução seguinte a chamada da rotina. 71

90 4.1. Porting do ADEOS para a Plataforma MCS-51 Mudança de Contexto Por sua vez, a rotina de mudança de contexto - contextswitch - basicamente salvaguarda o estado da tarefa atual em execução (à exceção da tarefa idle), e restaura o estado da que se pretende executar à posteriori. Com efeito, esta rotina apresenta uma estratégia de implementação baseada em seis ou dez etapas, dependendo da condição da tarefa que se encontra atualmente em execução. Caso seja a idle não é necessário guardar o estado da tarefa actual, resumindo-se portanto a rotina a seis etapas. O algoritmo 2 ilustra a estratégia utilizada. Algoritmo 2 Mudança de contexto no contextswitch contextswitch(...): aceder ao apontador do parâmetro old Context; if tarefa idle then; guardar o endereço do final da rotina; guardar as flags do processador; guardar o segmento da stack; guardar o segmento de dados; endif; aceder ao apontador do parâmetro new Context; restaurar o segmento de dados; restaurar o segmento da stack; restaurar as flags do processador; restaurar o endereço de retorno; O protótipo da função contextswitch (listagem 4.3) tem dois parâmetros de entrada, sendo estes os apontadores para a estrutura do contexto da tarefa atualmente em execução (poldcontext), e para a tarefa que se pretende que entre em execução 72

91 Capítulo 4. Implementação do Sistema (pnewcontext). A função tem retorno vazio (void). void contextswitch(context poldcontext, Context pnewcontext); Listagem 4.3: Protótipo da função contextswitch A primeira etapa da inicialização do contexto representa o prólogo da função. Este consiste em gravar o base pointer na pilha do sistema, atualizar esse base pointer depois de o ter gravado, e posteriormente, através da instrução les, aceder ao apontador para a estrutura do contexto da tarefa atualmente em execução (16-bit no destination index e 16-bit no extra segment). Além disso, com a utilização das instruções mov copia-se esses endereços para o registo data (dx) e para o acumulador (ax), para avaliar a a condição da tarefa idle. push bp mov bp, sp les di, dword ptr ss:[bp+6] mov dx, es mov ax, di O código assembly apresentado abaixo inicia o corpo da função. Consiste na verificação da tarefa atualmente em execução. Com o or-lógico verifica-se se ambos os endereços da estrutura da tarefa em execução são nulos, pois caso isso aconteça significa que a tarefa atualmente em execução é a idle, não sendo portanto necessário guardar o estado da mesma. or ax, dx jz fromidle Na segunda etapa inicia-se o processo de backup do estado da tarefa, isto é, preenche-se a estrutura de dados do contexto da tarefa atualmente em execução com o estado atual da tarefa. mov dx, cs lea ax, switchcomplete mov es:[di], ax mov es:[di+2], dx Assim, com a primeira instrução guarda-se o code segment, com a instrução lea obtém-se o endereço (offset) da label switchcomplete (16-bit apenas), e com as duas últimas duas instruções mov preenche-se o primeiro (IP) e segundo elemento (CS) da estrutura do contexto da tarefa (es:[di]) atual com o endereço do final da rotina. A terceira etapa guarda as flags do processador na estrutura do contexto da tarefa. Para isso, guarda as flags na pilha (pushf), e posteriormente preenche o terceiro elemento (Flags - es:[di+4]) da estrutura com esse valor. 73

92 4.1. Porting do ADEOS para a Plataforma MCS-51 pushf pop es:[di+4] A quarta etapa consiste no backup do segmento da pilha da tarefa. mov dx, ss mov es:[di+6], sp mov es:[di+8], dx Com a execução das duas últimas instruções mov preenche-se o quarto (SP - es:[di+6]) e quinto elemento (SS - es:[di+8]) da estrutura do contexto da tarefa com o stack pointer e stack segment. A quinta etapa é a última etapa destinada ao backup do estado da tarefa, nomeadamente o segmento de dados da mesma. mov dx, ds mov es:[di+10], si mov es:[di+12], dx Com a execução das duas últimas instruções mov preenche-se o sexto (SI - es:[di+10]) e sétimo elemento (DS - es:[di+12]) da estrutura do contexto da tarefa com os registos source index e data segment. A sexta etapa é a primeira destinada ao restauro do processador com a informação da tarefa que se pretende colocar em execução. fromidle: les di, dword ptr ss:[bp+10] mov dx, es mov ax, di Com a instrução les acede-se ao apontador para a estrutura do contexto da tarefa que irá entrar em execução, mais concretamente ao último elemento (SI - es:[di+10]) da mesma. As duas instruções mov efetuam o backup do apontador para o registo data (dx) e para o acumulador (ax). A sétima etapa restaura então o registo source índex do segmento de dados. Para isso, utiliza a instrução lds, colocando em si o sexto elemento (SI) da estrutura do contexto da nova tarefa. lds si, dword ptr [di+10]; si = pnewcontext >SI A oitava etapa consiste no restauro do segmento da pilha. mov dx, es:[di+8] mov ax, es:[di+6] pushf ; Save the current interrupt state. pop cx cli ; Disable interrupts. 74

93 Capítulo 4. Implementação do Sistema mov ss, dx mov sp, ax push cx popf ; Restore the saved interrupt state. Com efeito, as duas primeiras instruções colocam no registo de dados e no acumulador o quinto (SS) e quarto (SP) elementos da estrutura do contexto da nova tarefa, respetivamente. As três instruções seguintes permitem guardar o estado das flags e desabilitar as interrupções. Depois disso, são restaurados os registos sack segment e stack pointer, com as instruções mov. A etapa termina com o restauro das flags. Finalmente, o último segmento de código representa o epílogo da função, responsável por restaurar, de forma indireta, as flags do processador e do endereço de retorno. Por outras palavras, com a instruções push coloca na pilha o primeiro (IP), segundo (CS) e terceiro (Flags) elementos da estrutura do contexto da nova tarefa, e com a instrução iret retorna da rotina restaurando as flags simultaneamente. push es:[di+4] push es:[di+2] push es:[di] iret Porting do Código Dependente do Processador A segunda tarefa do processo de porting do ADEOS consiste então na substituição do código dependente da arquitetura por código assembly 8051, procurando manter, tanto quanto possível, a estratégia utilizada na versão original do sistema operativo. Obviamente que uma vez que os processadores têm arquiteturas dispares, será necessário efetuar algumas modificações. Neste sentido, de seguida serão apresentadas as alterações efetuadas pelo autor, assim como as estratégias utilizadas para a inicialização e mudança de contexto. Ficheiro Cabeçalho (bsp.h) No ficheiro cabeçalho bsp.h a primeira alteração surge desde logo com a alteração da estrutura do contexto (listagem 4.4). Como os microprocessadores têm arquiteturas diferentes, é compreensível que tenham registos e estados diferentes. Assim sendo, a estrutura apresentada abaixo implementa o contexto de uma tarefa do De toda a estrutura importa referenciar as variáveis PC H e PC L, XSP H e XSP L, que correspondem, respetivamente, ao endereço da memória da próxima instrução 75

94 4.1. Porting do ADEOS para a Plataforma MCS-51 de execução da tarefa e ao endereço da pilha da tarefa (em memória externa). Os restantes são registos intrínsecos ao estado do microprocessador. struct context unsigned char PC H, PC L; unsigned char A, B; unsigned char IE; unsigned char DPL, DPH; unsigned char R0, R1, R2, R3, R4, R5, R6, R7; unsigned char PSW; unsigned char SP; unsigned char XSP H, XSP L; }; Listagem 4.4: Definição da estrutura do estado da máquina (8051) de cada tarefa Também as macros para delimitação de secções críticas foram ligeiramente alteradas (listagem 4.5). Apesar da lógica ser a mesma, não existe instruções dedicadas para gravar as flags e desabilitar as interrupções, pelo que isso tem que ser feito com os respetivos registos. Portanto, sempre que se entra numa secação crítica o registo IE (0xA8) é colocado na pilha e é desabilitado o bit geral das interrupções. Não é utilizada a instrução CLR EA, pois o compilador não reconhece a flag. Por sua vez, quando sai da secção crítica, é feito o restauro através da pilha #define entercs()\ \ asm(\ PUSH 0xA8 \n \ ANL 0xA8, #0x7F \n \ );\ } #define exitcs()\ \ asm(\ POP 0xA8 \n \ );\ } Listagem 4.5: Macros para delimitação de uma secção crítica No protótipo das funções não existe nenhuma alteração na declaração, apenas é utilizada uma macro para redefinir a função de mudança de contexto (listagem 4.6). Isto é necessário devido á convenção da chamada de funções do compilador da IAR. Como na chamada de uma função os parâmetros são colocados nos registos do microprocessador (por questões de otimização), é então necessário guardar esses registos na pilha antes de invocar a função. 76

95 Capítulo 4. Implementação do Sistema #define ContextSwitch(old context, new context)\ \ asm( \ PUSH A \n \ PUSH 1 \n \ PUSH 2 \n \ PUSH 3 \n \ PUSH 4 \n \ PUSH 5 \n \ PUSH DPL \n \ PUSH DPH \n \ ); \ contextswitch(&old context, &new context); \ } \ Listagem 4.6: Macro para comutação de contexto (ContextSwitch) Ficheiro Assembly (bsp.asm) No ficheiro assembly bsp.asm é onde se verificam as principais alterações. Apesar deste continuar a ter a implementação das três funções declaradas no ficheiro cabeçalho, existem modificações consideráveis em duas delas. De seguida, serão apresentadas e explicadas as novas metodologias para inicialização e mudança de contexto, assim como as alterações na implementação das mesmas. Inicialização do contexto Relativamente à função de inicialização do contexto, esta apresenta agora uma estratégia de implementação baseada em oito etapas. Apesar de seguir a mesma abordagem que a anterior, é mais longa pois está mais detalhada a nível dos registos do estado do processador. O algoritmo 3 ilustra a estratégia utilizada. De forma a simplificar a explicação da implementação da função, convém clarificar, desde já, onde é que os parâmetros de entrada são colocados na chamada da função. Assim sendo, conforme foi apresentado na subsecção 3.4.1, o primeiro argumento, endereço para uma estrutura localizada em memória externa (64k-byte), é um endereço de 16-bit, pelo que é colocado nos registos R2 e R3 do banco 0. Por sua vez, o segundo argumento é um apontador para uma localização da memória de código (2 16 = 64k-byte), daí que seja um endereço de 16-bit colocado nos registos R4 e R5. O terceiro argumento é um apontador para o objeto tarefa, colocado na pilha externa (XSP), devido à inexistência de mais registos para variáveis de 16-bit. O código apresentado abaixo implementa a primeira e segunda etapa do processo 77

96 4.1. Porting do ADEOS para a Plataforma MCS-51 Algoritmo 3 Inicialização do contexto no contextinit contextinit(...): aceder ao apontador da estrutura context da tarefa; inicializar o apontador para a rotina de startup; inicializar o registo A e B; inicializar o registo de interrupões; inicializar o registo DPTR; inicializar os registo R0-R7; inicializar as flags do processador; inicializar o segmento da stack; de inicialização do contexto da tarefa. ;Get the pointer to context MOV DPH, 3; Load pcontext H into DPH MOV DPL, 2; Load pcontext L into DPL ;Initialize the pointer to startup routine MOV A, 5; A = pfunc H A; pcontext >PC H = pfunc H INC DPTR; point to pcontext >PC L MOV A, 4; A = pfunc L A; pcontext >PC L = pfunc L INC DPTR; point to pcontext >A As duas primeiras instruções (prólogo da função) permitem aceder ao apontador do contexto da tarefa. O restante código (início do corpo da função) inicializa o apontador para a rotina de startup da tarefa. Com as instruções MOVX inicializa-se o primeiro (PC H) e segundo (PC L) elemento da estrutura do contexto da tarefa, através de endereçamento indirecto para memória externa. A terceira etapa consiste na inicialização dos registos A e B. Seguindo a mesma linha da etapa anterior, com a utilização das instruções MOVX inicializa-se o terceiro (A) e quarto (B) elemento da estrutura do contexto da tarefa. ;Initialize A and B MOV A, #0; A = 0; 78

97 Capítulo 4. Implementação do Sistema A; pcontext >A = A (0) INC DPTR; point to pcontext >B... A inicialização do estado das interrupções acontece na quarta etapa. O estado actual das interrupções é salvaguardado na pilha (PUSH), as interrupções gerais e a do temporizador 0 são ativadas por defeito (valor 0x82), o quinto elemento da estrutura (IE) é inicializado com esse valor, e o estado anterior das interrupções é restaurado (POP). A activação da interrupção do timer 0 está ligada à metodologia utilizada na versão original para tornar as tarefas periódicas. ; Initialize interrupts PUSH 0xA8; Save IE in stack ORL 0xA8,#0x82; Enable Interrupts (Timer0 for clock tick) by default MOV A, 0xA8; A = IE; A; pcontext >IE = 0x82 POP 0xA8; Restore IE As próximas três etapas permitem inicializar o data pointer, os registos R0 a R7 e as flags do processador. A metodologia é exactamente a mesma das etapas anteriores, que consiste em aceder aos elementos seis a dezasseis (DPL a PSW) da estrutura da tarefa, e inicializar a nulo. No registo PSW isso significa limpar todas as flags, como por exemplo, a flag de carry (C) e paridade (P). ; Initialize DPTR INC DPTR; point to pcontext >DPL A; pcontext >DPL = 0 INC DPTR; point to pcontext >DPH A; pcontext >DPH = 0 ;Initialize Registers. INC DPTR; point to pcontext >R0 MOV A, #0; A; pcontext >R0 = 0... ;Initialize Processor Flags PUSH PSW; Save PSW in stack ANL PSW,#0x00; CLEAN ALL FLAGS MOV A, PSW; A = PSW; A; pcontext >PSW = 0x00 POP PSW; Restore PSW A oitava e última etapa da inicialização do contexto da tarefa permite inicializar as variáveis da estrutura do contexto da tarefa que armazenam a informação relativa ao segmento da stack. Por outras palavras, inicializam a variável SP com o endereço da pilha interna, bem como as variáveis XSP L e XSP H com o endereço da pilha externa. 79

98 4.1. Porting do ADEOS para a Plataforma MCS-51 Mudança de contexto Tal como na inicialização, também a rotina de mudança de contexto apresenta uma estratégia de implementação mais longa, quando comparada com a estratégia descrita na secção Mais uma vez, o processo é condicionado pela tarefa atual em execução. O algoritmo 4 apresenta essa estratégia. O código apresentado abaixo (em parte, prólogo da função) implementa a condição da tarefa em dois passos. MOV A, 2; put poldcontext L in A JNZ fromtask; if poldcontext L!= 0, no NULL pointer MOV A,3; put poldcontext H in A JNZ fromtask; if poldcontext H!= 0, no NULL pointer, goto fromtask CALL fromidle; NULL pointer, goto fromidle Caso a tarefa em execução seja a idle, então o apontador para o contexto dessa tarefa é nulo. Portanto, o código acima testa o LSB e MSB desse endereço, e só na eventualidade de ambos serem nulos é que salta para a etapa oito. Isso é conseguido com a instrução JNZ, que verifica se o valor do acumulador é nulo e salta para o endereço de código da label caso isso não aconteça. Se acontecer continua o fluxo normal de execução, sem efetuar nenhum salto. A segunda etapa implementa o primeiro estágio do backup da informação da tarefa em execução, isto é, salvaguarda o endereço da próxima instrução a executar assim que a tarefa volte a obter o controlo do processador. As etapas três a oito permitem gravar os registos, flags e interrupções do processador, bem como a pilha da tarefa. A metodologia de implementação é semelhante em todos os casos. Como esses registos são guardados na pilha (interna) antes da chamada da função contextswitch, consistem basicamente em aceder ao endereço da pilha que tem o estado do registo, e copiar essa informação para a respetiva estrutura. O código abaixo exemplifica para o caso do registo A do processador. ;Save A MOV A, SP; Save into ACC SP adress CLR C; Clear Carry to subtract SUBB A,#9; Point to the adress of ACC saved in stack MOV R1,A; R1 = adress ACC (saved) MOV A,@R1; A = A(saved into stack) INC DPTR; point to poldcontext >A A; poldcontext >A = A... Depois de efetuado o backup da informação da tarefa em execução, é então necessário restaurar o estado da nova tarefa. Como a execução de instruções afeta 80

99 Capítulo 4. Implementação do Sistema Algoritmo 4 Mudança de contexto no contextswitch contextswitch(...): aceder ao apontador do parâmetro old Context; if tarefa idle then; guardar o endereço de retorno; guardar o registo A e B; guardar o estado das interrupções; guardar o registo DPTR; guardar os registo R0-R7; guardar as flags do processador; guardar o segmento da stack; endif; aceder ao apontador do parâmetro new Context; restaurar o segmento da stack; restaurar o endereço de retorno restaurar o registo A e B; restaurar o estado das interrupções; restaurar o registo DPTR; restaurar os registo R0-R7; restaurar as flags do processador; 81

100 4.2. Upgrade do ADEOS registos no processador, a estratégia passa por reter a informação do novo estado na pilha (interna), e apenas restaurá-lo no processador no momento anterior ao retorno da função. A nona etapa é a primeira destinada ao restauro do estado da nova tarefa. Com as instruções MOV acede-se ao endereço na estrutura do contexto da nova tarefa, passado como argumento através dos registos R4 e R5. ;Get pnewcontext MOV DPL, 4; get pnewcontext MOV DPH, 5; get pnewcontext As etapas dez a quinze permitem restaurar o endereço de retorno, registos, flags e interrupções do processador. Conforme foi previamente explicado, esse restauro é feito em dois momentos, pelo que o código apresentado acima reflete esse primeiro estágio. Acede-se os elementos da estrutura da nova tarefa, e copia-se a informação para a pilha (interna). Só mais tarde é que essa informação é restaurada ao processador. ;Save the return address into stack INC DPTR; point to pnewcontext >PC L MOVX A,@DPTR; A = pnewcontext >PC L PUSH A; Save ACC (pnewcontext >PC L) into stack MOV DPL, 4; get pnewcontext MOV DPH, 5; get pnewcontext MOVX A,@DPTR; A = pnewcontext >PC H PUSH A; Save ACC (pnewcontext >PC H) into stack ;Save A and B into stack INC DPTR; point to pnewcontext >PC L INC DPTR; point to pnewcontext >A MOVX A,@DPTR; A = pnewcontext >A PUSH A... ;Save PSW into stack INC DPTR; point to pnewcontext >PSW MOVX A,@DPTR; A = pnewcontext >PSW PUSH A A etapa dezasseis reflete o restauro da pilha da tarefa (interna e externa). Depois disso, o último bloco de código (epílogo da função) faz o restauro sequencial da informação da nova tarefa. 4.2 Upgrade do ADEOS O upgrade de software é um processo gradual e progressivo, que requer tempo pois existe sempre alguma funcionalidade a implementar. O ADEOS não é excepção. 82

101 Capítulo 4. Implementação do Sistema Assim sendo, o upgrade de um sistema operativo podia, por si só, dar origem a uma dissertação. Como tal, o autor decidiu expandir e melhorar o sistema operativo em três aspetos: (1) clock-tick intrínseco ao escalonador; (2) device-drivers para os periféricos do 8051; (3) escalonador power-aware. O primeiro porque possibilita ao sistema operativo implementar estratégias de escalonamento com time-slice. O segundo porque os device drivers simplificam a interface com os periféricos do microcontrolador. Finalmente, o escalonador power-aware porque implementa uma estratégia de escalonamento tendo em vista a minimização do consumo, característica fundamental nos sistemas embebidos atuais. Outras funcionalidades como métodos de comunicação entre processos (message queue, shared memory, etc.), outras estratégias de escalonamento, ou mesmo uma pilha TCP/IP, podem ser implementadas de forma gradual, pois não são o foco central nem desempenham um papel crucial na presente dissertação Upgrade: clock-tick no escalonador Conforme mencionado na secção 3.2.2, um dos pontos de escalonamento acontece com o clock-tick dos temporizadores por software. A versão original do sistema operativo implementa temporizadores por software para gerir a periocidade e o estado das tarefas. Por outras palavras, sempre que esse clock-tick ocorre, o sistema operativo decrementa e verifica os temporizadores por software ativos, e caso algum termine as tarefas colocadas em estado waiting à espera dessa temporização são comutadas para o estado ready. Esta metodologia é bastante eficaz para o tipo de escalonador implementado, no entanto em escalonadores com time-slice esta abordagem é ineficaz. Neste sentido, como a tarefa do autor passa por criar a base do sistema operativo para o melhorar e aumentar gradualmente, este decidiu implementar um clock-tick intrínseco ao próprio escalonador, responsável por invocar o escalonador a cada timeslice. Desta forma é possível escalonar utilizando a abordagem dos temporizadores por software, ou então seguindo a estratégia de time-slice. Para implementar essa nova estratégia, convém primeiro definir uma nova interrupção desencadeada pelo trigger da interrupção do temporizador. O 8051 clássico dispõe de dois temporizadores. O temporizador 0 é utilizado para gerar a interrupção responsável pela gestão dos temporizadores por software. O temporizador 1 tem que ser então utilizado para desencadear o trigger responsável pelo time-slice. A tabela 4.1 apresenta a implementação da rotina de ISR invocada aquando da ocorrência do 83

102 4.2. Upgrade do ADEOS Tabela 4.1: Rotina de interrupção do temporizador 1 Método C++ CDP (8051) #pragma vector = TF1 int interrupt void Sched::tick(void) entercs(); } recharge sched tick( CYCLES PER TICK); os.schedule(); exitcs(); recharge sched tick: CODE MOV TL1,R2 MOV TH1,R3 RET overflow do temporizador 1, assim como a implementação assembly de recarregamento dos registos de contagem do temporizador. A definição da rotina de interrupção é feita com a macro pragma vector. A interrupção é embutida na classe definindo-a como estática na sua declaração. Na ocorrência da interrupção, o temporizador é novamente carregado com o valor da temporização pretendida, e o escalonador é invocado. Como é uma rotina de serviço à interrupção, é considerada uma zona crítica, daí que o código esteja delimitado pelas macros entercs() e exitcs(). De forma a tornar o código o mais portável possível, a função responsável pela reconfiguração da temporização é implementada diretamente em assembly, juntamente com o restante código dependente do processador (ficheiros bsp.asm). Basicamente, os registos do temporizador 1 são carregados com o valor do parâmetro de temporização passado na função através dos registos R2 e R3 (inteiro). Para além da especificação da rotina de serviço à interrupção, é necessário configurar o temporizador 1. Por exemplo, é preciso especificar a cadência (temporização) a que ocorre a interrupção, assim como a habilitação da mesma. A tabela 4.2 apresenta o método responsável pela configuração do temporizador responsável pelo clock-tick, assim como o respetivo CDP implementado em assembly. O código assembly configura o timer 1 para funcionar como temporizador de 16-bit, habilita a interrupção de overflow do respetivo temporizador, e carrega os registos de contagem com o valor para gerar o trigger da interrupção com a cadência temporal pretendida. Depois de configurado o clock-tick e definida a rotina de ISR, apenas é necessário colocar o correr a temporização. Para isso, é especificado o método run tick, responsável por iniciar a contagem no temporizador. O código C++ e assembly apresentados na tabela 4.3 especificam o método explicado anteriormente e a respetiva implementação. A implementação de baixo nível é bastante simples, e corresponde apenas à activação da flag TR1 (Timer 1 Run) no registo TCON. 84

103 Capítulo 4. Implementação do Sistema Tabela 4.2: Configuração do temporizador 1 Método C++ CDP (8051) void Sched::config tick() config sched tick( CYCLES PER TICK); } config sched tick: CODE ORL TMOD,#0x10 ORL IEN0,#0x88 MOV TL1,R2 MOV TH1,R3 RET Tabela 4.3: Inicialização da contagem do temporizador 1 Método C++ CDP (8051) void Sched::run tick() run sched tick(); } run sched tick: CODE ORL TCON,#0x40 RET Desta forma, a possibilidade de inclusão do clock-tick para o time-slice fica apenas restringindo a duas linhas de código. Antes da execução do método de inicialização do sistema operativo, são executados os métodos config tick e run tick (listagem 4.7). Omitindo a chamada desses métodos o sistema operativo não invoca a ISR responsável por esse clock-tick. Assim sendo, o sistema operativo fica preparado para algoritmos de escalonamento com time-slice, possibilitando, no futuro, a implementação, por exemplo, do escalonador round-robin com time-slice. void main(void) os.config tick(); os.run tick(); os.start(); } Listagem 4.7: Configuração do clock-tick do escalonador Upgrade: device drivers Um device driver é um componente de software que permite que aplicações de alto nível comuniquem e interajam com dispositivos de hardware. Por outras palavras, podem ser definidos como black boxes que permitem que um componente de hardware responda a uma determinada interface de programação. Estes escondem completamente os detalhes de como o dispositivo funciona, e disponibilizam apenas operações e chamadas padronizadas que atuam no hardware real [66]. Na versão original do ADEOS a interface ao hardware não utiliza a pura abstração 85

104 4.2. Upgrade do ADEOS associada ao conceito de device driver (que tem associado um modelo comum), apenas implementa controladores de hardware sob a forma de classes. Isto porque o projetista apenas pretendeu demonstrar como é que os dois dispositivos (porta série, temporizador) podiam ser implementados usando classes. Assim sendo, a ideia do autor passa então, numa primeira fase, por desenvolver os vários controladores de hardware como objetos representativos dos diversos periféricos do Contudo, futuramente definir-se-á um modelo para uma framework I/O, em que essa abstração é implementada com template metaprogramming. Desta forma, implementar-se-á a verdadeira abstração característica do modelo dos device drivers. Isto tudo para explicar o porquê da designação de device drivers atribuída aos controladores de hardware desenvolvidos para os vários periféricos - (i) PWM, (ii) UART, (iii) GPIO, (iv) I 2 C e (v) SPI - do Device Driver: PWM Pulse with modulation, ou em português, modulação por largura de pulso, é uma técnica que permite gerar sinais analógicos, recorrendo a hardware externo (filtro passa-baixo), a partir de sinais digitais. O controlo digital é usado para gerar uma onda quadrada, que alterna constantemente entre o estado ligado (on) e desligado (off ). A porção de tempo que o sinal está em estado on, relativamente ao seu período, é designado de largura de impulso (duty-cycle). Assim, controlando o tempo que o sinal está a on e off num determinado período de tempo, é possível obter diferentes valores analógicos. Por exemplo, num sinal com um período de 10ms e com uma tensão máxima de 5V, se a onda estiver 6ms em estado on (5V) e 4ms em estado off (0V), o valor médio analógico conseguido é de 3V. Este tipo de técnica é muito utilizada para controlar o brilho de LEDs e a velocidade de motores de corrente contínua (DC). PWM no 8051 O microcontrolador 8051, na versão AT89C51ID2 da Atmel, dispõe de quatro módulos de PWM configurados através do periférico PCA (Programmable Counter Array). O PCA consiste num temporizador/contador dedicado que serve de base para um vetor de cinco módulos de comparação/captura. Todos os módulos do PCA podem ser usados com saídas de PWM. A frequência da saída é comum a todos os módulos, pois depende da fonte de relógio do periférico: 86

105 Capítulo 4. Implementação do Sistema (i) frequência de relógio do microcontrolador com divisão por seis; (ii) frequência de relógio do microcontrolador com divisão por dois; (iii) overflow do timer 0; (iv) fonte externa através do pino P1.2. O valor do duty-cycle de cada módulo é independente e variável (registo CCAPLn). Quando o valor do contador do PCA é inferior ao valor carregado no registo do módulo, a saída permanece em baixo, no entanto quando esse valor é igual ou superior então a saída é ativada. Quando o registo de temporização do PCA atinge o overflow, o valor do registo CCAPLn é carregado com o valor do registo CCAPHn. Isto permite fazer atualização do valor do PWM sem a ocorrência de falhas no sinal. Os bits de PWMn e ECOMn devem ser ativados no registo CCAPMn para selecionar o modo pretendido. PWM DD: Design O diagrama de classes da figura 4.4 representa a estrutura de classes do driver de PWM. Este é composto por uma classe principal, uma estrutura de configuração e várias enumerações. Figura 4.4: Diagrama de classes do driver PWM A classe tem quatro atributos e dez métodos. Relativamente aos atributos, dois deles são atributos da instância e dois atributos da classe (atributos estáticos). Os atributos da instância permitem caracterizar cada objeto com a especificação do módulo 87

106 4.2. Upgrade do ADEOS (module) e o valor do duty-cycle (dutycycle). Os atributos da classe permitem gerir e garantir a unicidade dos módulos de PWM. No que diz respeito aos métodos, estes também podem ser caracterizados como métodos da instância e métodos da classe. Assim, a classe Pwm8051 possui sete métodos da instância e três métodos da classe. Os primeiros permitem instanciar e configurar um objeto, enquanto os outros permitem gerir os módulos, de modo a não permitir tanto a instanciação de módulos de PWM já criados, como ultrapassar o limite máximo de módulos de PWM existentes no periférico. A estrutura pwm8051 config é utilizada para configurar cada um dos módulos da sua instanciação ou configuração. Com esta metodologia, apenas é passado o apontador da estrutura no chamada do método, garantindo assim melhor desempenho. Isto porque o compilador em vez de colocar todos os argumentos na pilha ou em registos, coloca apenas o apontador da estrutura. Quando a configuração requer poucos parâmetros a diferença de performance não é assim tão acentuada, no entanto em drivers com muitos parâmetros de configuração a diferença é considerável. A utilização das diversas enumerações permite adicionar alguma portabilidade ao código a desenvolver. Exemplificando, caso outra versão do microcontrolador utilize o valor 0x60 em vez de 0x40 para ativar o módulo de PWM, então basta modificar esse valor na enumeração sem ter de modificar todos os segmentos de código que o usam. PWM DD: Implementação A utilização da orientação a objetos no desenvolvimento de software tem crescido exponencialmente, muito por causa da simplicidade e transparência na passagem do design para a implementação. Se o software for bem desenhado utilizando as técnicas de UML, a implementação torna-se muito clara e fidedigna. Portanto, o código da listagem 4.8 representa a declaração da classe especificada no diagrama da figura 4.4. class Pwm8051 public: Pwm8051(); Pwm8051(pwm8051 Config Config); Pwm8051(); void config(pwm8051 Config Config); void config DC module(unsigned char dc); void config Freq module(pwm freq freq); void enable module(pwm en enable); static bool check module(unsigned char module); 88

107 Capítulo 4. Implementação do Sistema }; static void enable ALL(pwm en enable); static void config Freq ALL(pwm freq freq); private: unsigned char module; unsigned char dutycycle; enum max pwm modules = 4}; static unsigned char modules[]; static unsigned char num modules; Listagem 4.8: Declaração da classe Pwm8051 Na classe apresentada acima, utiliza-se a técnica de encapsulamento. Portanto, os métodos são declarados como públicos, enquanto os atributos como privados. Os primeiros três métodos representam o construtor e destrutor da classe. Os quatro métodos seguintes permitem fazer a configuração total ou parcial do device driver, isto é, permitem configurar o duty-cycle, frequência e activação de cada um dos módulos. Os últimos três métodos são os métodos da classe, pois permitem a configuração de todos os módulos simultaneamente, e não de cada instancia ou módulo em particular. Os primeiros dois atributos permitem configurar cada módulo. A enumeração limita o número máximo de módulos do periférico. Os últimos dois atributos da classe permitem fazer essa gestão dos módulos. A estrutura que permite fazer a configuração do driver é apresentada na listagem Esta é composta por dois elementos, que permitem configurar qual o módulo que se pretende instanciar (0 a 4) e o respectivo valor do duty-cycle (0 a 255, em que 255 corresponde a estar sempre activo). typedef struct pwm8051 config unsigned char dc; unsigned char module; }pwm8051 Config; Listagem 4.9: Estrutura de configuração da classe Pwm8051 A definição das enumerações que contém a informação dos parâmetros específicos deste hardware é apresentada na listagem enum pwm freq f osc2 = 0x02, f osc6= 0x00, f timer = 0x04, f ext = 0x06}; enum pwm en enable = 0x40, disable = 0x00}; enum pca pwm pwm 8bit = 0x42, none = 0x00}; Listagem 4.10: Enumerações da classe Pwm8051 Sempre que um novo objeto é instanciado, o periférico PCA é configurado para funcionar no modo PWM. Depois, com o método de configuração total (config) é 89

108 4.2. Upgrade do ADEOS possível configurar o módulo criado (listagem 4.11). Esta recebe como parâmetro o apontador da estrutura de configuração, que permite inicializar os atributos module e dutycycle intrínsecos ao objecto. As duas linhas de código seguintes adicionam esse módulo aos atributos da classe. Por fim, é inicializado o registo de configuração do duty-cycle com o valor pretendido. void Pwm8051::config(pwm8051 Config Config) module = Config >module; dutycycle = (255 Config >dc); modules[num modules] = module; num modules++; switch(module) case 0: CCAP0H=DutyCycle; //Reload value to Duty Cycle break; case 1: CCAP1H=DutyCycle; //Reload value to Duty Cycle break; case 2: CCAP2H=DutyCycle; //Reload value to Duty Cycle break; case 3: CCAP3H=DutyCycle; //Reload value to Duty Cycle break; } } Listagem 4.11: Método config da classe Pwm8051 A implementação dos restantes métodos pode ser consultada no código fonte do driver PWM desenvolvido. Device Driver: UART Universal Asynchronous Receiver/Transmitter (UART) é um transmissor/receptor full-duplex que fornece toda a lógica para a transferência assíncrono, isto é, é um componente de hardware que converte e formata os dados entre as formas série e paralela. Estes geralmente são usados em conjunto com normas de comunicação, nomeadamente RS-232, RS-422 ou RS-485. A designação universal indica que o formato de dados e as velocidades de transmissão são configuráveis, e que os níveis de tensão dos sinais elétricos são convertidos por um circuito externo, como por exemplo o MAX232 [67]. 90

109 Capítulo 4. Implementação do Sistema UART no 8051 A porta série integrada no AT89C51ID2 é compatível com a porta série da família MCS-51. Assim, esta permite a transmissão no modo full-duplex, e pode funcionar em vários modos e frequências. A sua principal função assenta, portanto, na conversão paralelo-série dos dados a serem transmitidos e conversão série-paralelo dos dados recebidos. O hardware da porta série pode ser acedido através dos pinos TxD (transmissão) e RxD (receção), e apresenta um buffer que permite a receção de um segundo carácter antes da leitura do primeiro. Assim, a receção dos caracteres é efetuada através da leitura do registo SBUF, enquanto o envio de um carácter é realizado pela escrita no mesmo registo. A porta série fornece quatro modos de funcionamento, programados através da escrita nos bits SM0 e SM1 do registo SCON. Este registo contém também os bits de estado e controlo da mesma. Os modos 1, 2 e 3 permitem a comunicação assíncrona, com os bits de dados encapsulados entre o start e stop bit. O modo 0 é síncrono e a porta série funciona como um registo de deslocamento. Nos modos 1 e 3 o baud rate é variável e pode ser gerado pelo temporizador 1 ou 2. Os modos 2 e 3 permitem a comunicação entre vários processadores 8051, usando o modelo de multiprocessamento mestre-escravo. Para isso basta ativar o bit SM2 do registo SCON. UART DD: Design O diagrama de classes da figura 4.5 representa a estrutura de classes do driver UART. Este é composto por duas classes, uma estrutura de configuração e várias enumerações. A classe Uart8051 tem dois atributos e oito métodos. Os atributos, ambos da instância, permitem definir um apontador para um buffer de receção e transmissão, responsáveis por reter os dados da comunicação. No que diz respeito aos métodos, os primeiros quatro permitem instanciar e configurar um objeto UART, enquanto os outros permitem iniciar o processo de transmissão e receção, e obter o tamanho de cada um dos buffers. A classe Buffer tem cinco atributos e oito métodos. Relativamente aos atributos, o primeiro (array) é um apontador para o primeiro elemento do buffer, o segundo (size) define o tamanho do buffer, o terceiro (head) e quarto (tail) permitem gerir os elementos do mesmo, e, finalmente, o quinto atributo (count) permite saber o número de itens presentes no buffer. Os métodos, os primeiros correspondem ao 91

110 4.2. Upgrade do ADEOS Figura 4.5: Diagrama de classes do driver UART construtor e destrutor do objeto, enquanto os restantes fazem a gestão do buffer, como por exemplo adicionar e remover itens do mesmo. A estrutura uart8051 config é utilizada para configurar a porta série no momento da sua instanciação ou configuração. Desta forma, é possível configurar por exemplo o baud rate, modo de operação (síncrona ou assíncrona), receção, e buffers. Com a utilização desta metodologia, o ganho de desempenho é relativamente maior que no caso anterior (driver PWM), pois, tal como foi explicado, o número de parâmetros que seriam passados como argumentos da função é consideravelmente superior. Mais uma vez, a utilização das enumerações para especificar os valores dos registos na configuração da porta série, permite adicionar alguma portabilidade e clareza ao código a desenvolver. UART DD: Implementação Na classe Uart8051 (listagem 4.12) é utilizado o encapsulamento para garantir a integridade dos dados contidos no objeto. Portanto, os métodos são declarados 92

111 Capítulo 4. Implementação do Sistema como públicos enquanto os atributos como privados. Os primeiros três métodos representam o construtor e destrutor da classe. Os sete métodos seguintes permitem fazer a configuração total ou parcial do device driver, isto é, permitem configurar, por exemplo, o baud rate, o modo de funcionamento (síncrono ou assíncrono), e a multicomunicação. Os últimos quatro métodos permitem desencadear a transmissão e receção dos dados, assim como saber o número de elementos de cada um dos buffers (receção e transmissão) class Uart8051 public: Uart8051(); Uart8051(uart8051 Config Config); Uart8051(); void config(uart8051 Config Config); void config baudrate(uart baud baudrate); void config mode(uart mode mode); void config reception(uart reception reception); void config multicom(uart multicom multicom); void config TX b8(uart tx b8 TX b8); void config RX b8(uart tx b8 RX b8); void txstart(void); void rxstart(void); int get tx buf size() return ptx buf >getsize();} int get rx buf size() return prx buf >getsize();} }; private: Buffer ptx buf; Buffer prx buf; Listagem 4.12: Declaração da classe Uart8051 A estrutura que permite fazer a configuração do driver é composta por oito membros, que permitem configurar o baud rate (4800, 9600, 19200, 28800,...), o modo de operação (modo 0,1,2 ou 3), a ativação da receção, a multicomunicação, e o 8- bit de dados da transmissão e receção. Os elementos ptx buf e prx buf definem apontadores para os buffers de transmissão e receção. A implementação do construtor da classe que permite a configuração da porta série é apresentado na listagem Este recebe como parâmetro o apontador da estrutura de configuração. As primeiras oito linhas de código permitem configurar o valor do baud rate, no entanto apenas nos modos onde isso é possível (modos 1 e 3). O temporizador 2 é definido como o gerador de baud rate pois os temporizadores 0 e 1 já são utilizados para outras funções do sistema operativo. O registo SCON é 93

112 4.2. Upgrade do ADEOS configurado com as funcionalidades pretendidas. Os elementos ptx buf e prx buf, intrínsecos a classe, são inicializados com os apontadores pretendidos. Uart8051::Uart8051(uart8051 Config Config) if(config >mode == mode1 Config >mode == mode3) int baud value; T2CON = 0x34;//timer 2 baud rate generator baud value = (int)(65535) (F OSC/(32 Config >baudrate)); RCAP2H = (baud value&0xff00)>>8; RCAP2L = (baud value&0x00ff); } SCON = Config >mode Config >reception Config >multicom Config >tx b8 Config >rx b8; ptx buf = Config >ptx buf; prx buf = Config >prx buf; } Listagem 4.13: Construtor da classe Uart8051 com configuração Os métodos apresentados na listagem 4.14 implementam a transmissão e receção de dados na porta série. A transmissão consiste em colocar no registo SBUF um elementos do buffer de transmissão, e esperar que a flag de conclusão de transmissão (TI) seja ativa. Para enviar n elementos, repete-se o processo n vezes. A recepção é o processo inverso. Aguarda-se que a flag de conclusão de recepção (RI) seja ativada, e coloca-se o elemento recebido no buffer de receção. Para receber n elementos, repete-se o processo n vezes. void Uart8051::txStart(void) SBUF = tx buf >remove(); while(!(scon&ti)); //while TI=0 SCON &= TI;//Clean TI } void Uart8051::rxStart(void) while(!(scon&ri));//while RI=0 SCON &= RI;//Clean RI rx buf >add(sbuf); } Listagem 4.14: Métodos txstart e rxstart da classe Uart8051 A implementação dos restantes métodos, assim como a implementação da classe Buffer, pode ser consultada no código fonte do driver UART desenvolvido. 94

113 Capítulo 4. Implementação do Sistema Device Driver: GPIO General Purpose Input/Output (GPIO), ou em português, entradas/saídas de propósito geral, podem ser designadas como pinos genéricos presentes em chips cujo comportamento (incluindo a definição de entrada ou saída) pode ser controlador por software. Este tipo de hardware é muito utilizado em integrados multifunções (por exemplo, codecs de áudio, placas de vídeo) ou em aplicações embebidas (por exemplo, Arduino) para leitura de sensores (temperatura, aceleração, orientação) ou controlo de motores de corrente continua e brilho de LEDs. As capacidades de um pino de GPIO incluêm a configuração da direção (entrada ou saída), máscara (ativos ou inativos), valores de entrada e saída, e configuração de interrupções. Um grupo de pinos GPIO, tipicamente 8 pinos, é designado como um porto GPIO. GPIO no 8051 Todos os registos de controlo de periféricos do 8051 estão mapeados na memória de dados interna, concretamente na área do SFR. Assim sendo, as quatro portas de entrada/saída possuem quatro registos de 8-bit que permitem controlá-los: P0, P1, P2 e P3. Cada um destes registos possui latches e hardware de interface às saídas (output drivers) e de leitura das entradas (input buffers) que permitem implementar as funcionalidades necessárias a uma porta de entrada/saída digital. As oito linhas de cada uma destas portas I/O podem ser tratadas individualmente, de modo a realizar a interface a dispositivos de 1-bit, ou então como unidades para realizar a interface paralela de 8-bit a outros dispositivos. Por defeito todos os pinos estão definidos como entradas digitais. Sempre que se pretende definir um pino como saída, é necessário ativar a respetiva latch, ou seja, escrever o valor lógico 1. Só depois de definido como saída é que o pino pode ser especificado como saída a nível lógico alto ou baixo. GPIO DD: Design O diagrama de classes da figura 4.6 representa a estrutura de classes do driver GPIO. Este é composto por uma classe principal, uma estrutura de configuração e várias enumerações. A classe tem cinco atributos e sete métodos. Relativamente aos atributos, três são atributos da instância enquanto os outros são atributos da classe (atributos estáticos). Os atributos da instância permitem caracterizar cada objeto com a definição da porta (port), pino (pin) e direção (direction). Os atributos da classe permitem gerir e 95

114 4.2. Upgrade do ADEOS Figura 4.6: Diagrama de classes do driver GPIO garantir a unicidade dos pinos. No que diz respeito aos métodos, existem também métodos da instância e métodos da classe. Assim, a classe Gpio8051 possui seis métodos da instância e um método da classe. Os primeiros permitem instanciar e configurar um objeto GPIO, enquanto o método da classe permite fazer a gestão dos mesmos, isto é, garantir não só que não é instanciado nenhum pino já utilizado, assim como um pino que não exista. A estrutura gpio8051 Config é utilizada para configurar o pino de GPIO no momento da sua instanciação ou configuração. Desta forma, é possível configurar por exemplo o porto, o pino e a direção. GPIO DD: Implementação Na classe Gpio8051 (listagem 4.15) os primeiros três métodos representam o construtor e destrutor da classe. Os três métodos seguintes permitem fazer a configuração total ou parcial do device driver, isto é, permitem configurar, por exemplo, a porta, pino e direção. O último, método da classe, permite verificar, antes da configuração, se um determinado pino de GPIO é válido. Os primeiros três atributos permitem a sua configuração. Os últimos dois atributos da classe permitem fazer essa gestão dos 96

115 Capítulo 4. Implementação do Sistema pinos. class Gpio8051 public: Gpio8051(); Gpio8051(gpio8051 Config Config); Gpio8051(); void config(gpio8051 Config Config); void config direction(gpio direction direction); bool config output(gpio out value); static bool check gpio(gpio8051 Config Config); }; private: unsigned char port; unsigned char pin; unsigned char direction; static unsigned char gpios[max gpio]; static unsigned char num gpios; Listagem 4.15: Declaração da classe Gpio8051 A estrutura que permite fazer a configuração do driver é composta por três elementos, que permitem configurar qual a porta (p0 a p3), o pino (0 a 7), e a direção (input ou output) do pino de GPIO. Sempre que um objeto do tipo pino é instanciado este deve ser devidamente configurado. A listagem 4.16 apresenta a implementação do método de configuração. Esta recebe como parâmetro o apontador da estrutura de configuração, que permite inicializar os atributos port, pin e direction intrínsecos ao objecto. As duas linhas de código seguintes adicionam esse módulo aos atributos da classe. Por fim, é especificado no hardware o valor do registo para configuração da direção do pino. void Gpio8051::config(gpio8051 Config Config) port = Config >port; pin = Config >pin; direction = Config >direction; gpios[num gpios] = (port<<4) (pin); num gpios++; switch(port) case p0: if(direction == input) P0 =(1<<pin); else P0&= (1<<pin); break; case p1: if(direction == input) P1 =(1<<pin); else P1&= (1<<pin); 97

116 4.2. Upgrade do ADEOS } } break; case p2: if(direction == input) P2 =(1<<pin); else P2&= (1<<pin); break; case p3: if(direction == input) P3 =(1<<pin); else P3&= (1<<pin); break; Listagem 4.16: Método config da classe Gpio8051 A implementação dos restantes métodos pode ser consultada no código fonte do driver GPIO desenvolvido. Device Driver: I 2 C Inter-Integrated Circuit (I 2 C) é um protocolo de comunicação bidirecional desenvolvido e patenteado pela Philips (atual NXP), de forma a reduzir os custos de fabrico dos dispositivos eletrónicos. Isto porque os dispositivos utilizam apenas duas linhas para a comunicação (interface série), permitindo a comunicação utilizando um número reduzido de pinos. As duas linhas utilizadas pelo barramento I 2 C são a SCL (Serial Clock) e SDA (Serial Data). A linha SDA é responsável por transportar os dados, enquanto a linha SCL sincroniza a transferência dos mesmos. Os dispositivos I 2 C podem ser classificados como mestre (master) ou escravos (slave). Um dispositivo que inicia a comunicação é designado por master, enquanto um dispositivo que responde às mensagens é denominado por slave. Um dispositivo pode ser unicamente master, unicamente slave, ou então comutar entre master e slave, dependendo da finalidade da aplicação. Normalmente, a velocidade de comunicação corresponde a 100k-bit/s para modo standard, 400k-bit/s para o modo fast e 3.4M-bit/s para o modo high-speed. [68] A figura 4.7 ilustra o formato da trama I 2 C. A comunicação inicia-se com o envio da condição de start pelo dispositivo master: enquanto a linha SCL está a nível lógico alto ( 1 ), a linha de SDA é colocada a nível lógico baixo ( 0 ). Depois disso, são enviados 7-bit com o endereço do dispositivo slave, mais 1-bit para definir se é uma operação de leitura ( 1 ) ou escrita ( 0 ). A transmissão é confirmada com o envio de um acknowledge (linha SDA a 0 ) pelo dispositivo slave. A etapa seguinte consiste no envio do byte de dados. Caso seja bem sucedido o slave envia novo acknowledge. 98

117 Capítulo 4. Implementação do Sistema Posto isso, ou são enviados dados continuamente, ou então é sinalizada a condição de paragem por parte do master. Essa condição consiste em colocar ambas as linhas de comunicação a nível lógico alto. Figura 4.7: Formato da trama I 2 C I 2 C no 8051 No 8051 clássico, não existe uma implementação por hardware do protocolo de comunicação I 2 C. No entanto, com o aumento exponencial da utilização do mesmo, os fabricantes decidiram implementá-lo em algumas das versões mais modernas. Assim, o AT89C51ID2 da Atmel é um exemplo onde este está presente. Neste microcontrolador, o protocolo está implementado com a designação TWI (2-wire interface). Isto porque a NXP patenteou o nome I 2 C, pelo que os outros fabricantes implementam um protocolo análogo com uma designação diferente. Tal como o I 2 C, o TWI utiliza duas linhas para comunicação, SCL e SDA, que são responsáveis pela transferência e sincronização da informação entre os dispositivos. O CPU controla a lógica do protocolo através de quatro registos especiais: SSCON (Synchronous Serial Control); SSDAT (Synchronous Serial Data); SSCS (Synchronous Serial Control and Status); e SSADR (Synchronous Serial Address). Estes registos permitem definir quatro modos de operação: (i) master transmitter; (ii) master receive; (iii) slave transmitter; e (iv) slave receive. O registo SSCON é usado para ativar a interface TWI, programar a taxa de transferência, ativar o modo slave, assinalar ou não a receção de dados, e enviar a condição de start ou stop. O registo SSCS especifica o estado da lógica e barramento do protocolo. Existem 26 possibilidades diferentes. Estes códigos podem ser consultados com mais detalhe do datasheet do microcontrolador [69]. O registo SSDAT contém o byte de dados série a ser transmitido ou recebido. Por outras palavras, antes de 99

118 4.2. Upgrade do ADEOS desencadear e iniciar uma transmissão é necessário carregar o byte para o registo. Por outro lado, sempre que uma receção é concluída, é necessário ler o byte deste registo. Finalmente, o registo SSADR é responsável por definir o endereço (7-bit) do dispositivo sempre que este é definido como slave. I 2 C DD: Design O diagrama de classes da figura 4.8 representa a estrutura de classes do driver I 2 C. Este é composto por uma classe principal, uma estrutura de configuração e várias enumerações. Figura 4.8: Diagrama de classes do driver I 2 C A classe I2c8051 tem três atributos e dezoito métodos. Os atributos, ambos da instância, permitem caracterizar o objeto I 2 C, nomeadamente o modo de funcionamento e o endereço (seja ele o próprio endereço, no caso de ser slave, ou então o endereço do dispositivo com o qual pretende comunicar, no caso de ser master). No que diz respeito aos métodos, os primeiros nove permitem instanciar e configurar um objeto I 2 C, enquanto os outros permitem activar, iniciar, enviar, receber e parar a 100

119 Capítulo 4. Implementação do Sistema transferência de dados. Por exemplo, o método start envia a condição de start do protocolo, o método send address o endereço do dispositivo com o qual se pretende comunicar, e o método read char recebe um byte de um dispositivo slave. A estrutura i2c8051 Config é utilizada para configurar o dispositivo I 2 C no momento da sua instanciação ou configuração. Desta forma, é possível configurar, por exemplo, o modo (master ou slave), o sentido da comunicação (escrita ou leitura), o endereço e a taxa de transferência de dados. I 2 C DD: Implementação Na classe I2c8051 (listagem 4.17) os primeiros três métodos representam o construtor e destrutor da classe. Os seis métodos seguintes permitem fazer a configuração total ou parcial do device driver, isto é, permitem configurar, por exemplo, o modo, o endereço e velocidade da comunicação. Os últimos métodos permitem a ativação (enable), iniciação(start e rstart), envio (send address e write char), receção (read address e read char) e paragem (stop) da transferência de dados. Os atributos permitem configurar os dispositivos I 2 C. class I2c8051 public: I2c8051(); I2c8051(i2c8051 Config Config); I2c8051(); void config(i2c8051 Config Config); void config mode(i2c mode mode); void config rw(i2c rw rw); void config adress(unsigned char addr); void config rate(i2c rate rate); void config assertack(i2c assert ack assert ack); void enable(i2c en enable); void start(); void rstart(); void stop(); bool send address(); bool read address(); bool write char(unsigned char c); bool read char(unsigned char c); void end read char(); private: i2c mode mode; i2c rw rw; unsigned char address; 101

120 4.2. Upgrade do ADEOS }; Listagem 4.17: Declaração da classe I2c051 A estrutura que permite fazer a configuração do driver é composta por cinco elementos, que permitem configurar o modo, o sentido (leitura ou escrita), o endereço, a taxa de transferência e o envio de confirmações (acknowledges). A implementação do construtores default da classe é apresentado na listagem Sempre que um dispositivo I 2 C é instanciado este é configurado como dispositivo master de escrita, cujo endereço do dispositivo slave com o qual pretende comunicar é 0x00. Por defeito, o registo de controlo SSCON é configurado de modo a desabilitar o módulo I 2 C, a taxa de transmissão igual à frequência do relógio do CPU com préescalar de 256, e envio de acknowledge. I2c8051::I2c8051() address = 0x00; rw = write; mode = master; SSCON = fclk 256 not en assert; } Listagem 4.18: Construtor por defeito da classe I2c051 Os métodos apresentados na listagem 4.19 implementam tanto o envio da condição de start como o envio de um byte utilizando o protocolo I 2 C. O envio da condição de start consiste em habilitar a respectiva flag no registo de controlo e aguardar que o registo de estado (SSCS) sinalize o sucesso no envio. Por sua vez, para o envio de dados é necessário preencher o registo SSDAT com o byte a enviar, e aguardar que o registo de estado sinalize a transmissão correta, ou então notifique a ocorrência de alguma anomalia. Daí que o método retorne verdade em caso de receção de acknowledge, ou falso caso isso não aconteça. void I2c8051::start() SSCON&= isr; //Clear SI interrupt SSCON =start ; //TWI start sending do }while(sscs!= start t);//wait to transmitt ACK SSCON&= start ; //Clear start Condition } bool I2c8051::write char(unsigned char c)//return 1 OK, return 0 Error SSCON&= isr; //Clear SI interrupt 102

121 Capítulo 4. Implementação do Sistema } SSDAT = c; do //Wait Data byte has been transmitted and ACK returned }while(sscs!= data t ack r && SSCS!= data t nack r && SSCS!= arbitation lost); if(sscs == data t ack r) return true; } else return false; } Listagem 4.19: Métodos start e write char da classe I2c8051 A implementação dos restantes métodos pode ser consultada no código fonte do driver I 2 C desenvolvido. Device Driver: SPI Serial Peripheral Interface Bus (SPI) é um protocolo de comunicação série síncrono, desenvolvido pela Motorola, que opera no modo full-duplex. Muitas vezes é também designado por protocolo four-wire, isto porque utiliza quatro linhas para a comunicação: SCLK (Serial Clock); MOSI ou SIMO (Master Out Slave In); MISO ou SOMI (Master In Slave Out); e SS (Slave Select). As linhas de MOSI e MISO são responsáveis pela transferência dos dados, a linha SCLK pela sincronização da transferência, e a linha SS pela seleção do dispositivo. Assim, neste protocolo existe um dispositivo master e um ou mais dispositivos slave. Se existir mais do que um dispositivo slave no sistema, então são necessárias tantas linhas de seleção quantos os dispositivos (figura 4.9a [70]). A figura 4.9b [71] ilustra o diagrama temporal do protocolo. Sempre que o dispositivo master pretende iniciar a comunicação este seleciona o dispositivo slave desabilitando (nível lógico 0 ) a respetiva linha de SS. Depois disso, habilita o sinal de relógio (SCLK) com uma frequência inferior à frequência máxima do dispositivo slave (tipicamente entre 1 a 30MHz). A polaridade do sinal de relógio pode ser ajustada com as opções CPOL e CPHA. A comunicação é full-duplex, pelo que o master envia um byte para o slave enquanto recebe também um byte do mesmo. Quando não existirem mais dados para serem transmitidos, o dispositivo master interrompe o sinal de relógio. Tipicamente o que acontece é manter o sinal de relógio ativo e 103

122 4.2. Upgrade do ADEOS habilitar (nível lógico 1 ) a linha de SS. (a) Barramento SPI: um master e três slaves independentes (b) Diagrama temporal do protocolo SPI Figura 4.9: SPI: barramento e diagrama temporal SPI no 8051 No 8051 clássico, também não existe uma implementação por hardware do protocolo de comunicação SPI. No entanto, tal como fizeram com o protocolo I 2 C, os fabricantes decidiram implementá-lo em algumas das versões mais modernas. O AT89C51ID2 é exemplo disso. Neste microcontrolador, os módulos de SPI incluem comunicação full-duplex, operação no modo master ou slave, oito taxas de transferência programáveis, sinal de relógio com polaridade e fase programáveis, e proteção contra colisões. O CPU controla a lógica do protocolo através de três registos especiais: SPCON (Serial Peripheral Control); SPSTA (Serial Peripheral Status); e SPDAT (Serial Peripheral Data). O registo SPCON é usado para ativar a interface SPI, configurar o modo de operação, programar a frequência de transferência, e selecionar a polaridade e fase do sinal de relógio. O registo SPSTA contém as flags que traduzem o estado da lógica e barramento do protocolo. Por exemplo, se os dados foram transferidos com sucesso é ativada a flag SPIF (Serial Peripheral Data Transfer Flag), enquanto se houver uma colisão de informação é ativada a flag WCOL (Write Collision Flag). Finalmente, o registo SPDAT representa o buffer de escrita/leitura para a receção de dados. Uma escrita para este registo coloca os dados diretamente no shift register. 104 SPI DD: Design

123 Capítulo 4. Implementação do Sistema O diagrama de classes da figura 4.10 representa a estrutura de classes do driver SPI. Este é composto por uma classe, uma estrutura de configuração e várias enumerações. Figura 4.10: Diagrama de classes do driver SPI A classe tem seis atributos e dezassete métodos. Relativamente aos atributos, quatro são atributos da instância, enquanto dois são atributos da classe. Os atributos da instância permitem caracterizar cada objeto com a especificação do modo (mode), operação (rw), endereço (address) e linha de seleção (chip select). Os atributos da classe permitem gerir e garantir a unicidade dos módulos SPI, mais concretamente, a linha de seleção. No que diz respeito aos métodos, a classe Spi8051 possui dezasseis métodos da instância e unicamente um método da classe. Os primeiros dez permitem instanciar e configurar um objeto, enquanto os restante permitem activar, iniciar, enviar, receber e parar a transferência de dados. O único método da classe permite gerir as linhas de seleção, de forma a que são seja instanciados objetos de dispositivos 105

124 4.2. Upgrade do ADEOS com a mesma linha de seleção. A estrutura spi8051 Config é utilizada para configurar o dispositivo SPI no momento da sua instanciação ou configuração. Desta forma, é possível configurar por exemplo o modo (master ou slave), a operação (escrita ou leitura), a taxa de transferência de dados, e a polaridade e fase do sinal de sincronismo. SPI DD: Implementação Na classe Spi8051 (listagem 4.20) os primeiros três métodos representam o construtor e destrutor da classe. Os sete métodos seguintes permitem fazer a configuração total ou parcial do device driver, isto é, permitem configurar, por exemplo, o modo, a operação, e a polaridade e fase do sinal de relógio. Os restantes métodos da instancia permitem a ativação, envio e receção de dados. O último método é o método da classe, responsável por verificar a unicidade de cada instância. Os primeiros quatro atributos permitem a configuração do dispositivo. A enumeração limita o número máximo de dispositivos SPI (limitado ao número de linhas de seleção). Os últimos dois atributos da classe permitem fazer essa gestão das linhas de seleção. class Spi8051 public: Spi8051(); Spi8051(spi8051 Config Config); Spi8051(); void config(spi8051 Config Config); void config mode(spi mode mode); void config clk pol(spi clk pol clk pol); void config clk phase(spi clk phase clk phase); void config rate(spi rate rate); void config RW(spi rw rw); void config address(unsigned char addr); void enable(spi enable enable); bool send address(); int read address(); bool write char(unsigned char c); bool read char(unsigned char c); void enablecs(bool value); static bool check Device(unsigned char CS); private: spi mode mode; spi rw rw; unsigned char address; unsigned char chip select; enum max spi devices = 7}; static unsigned char devices[]; 106

125 Capítulo 4. Implementação do Sistema }; static unsigned char num devices; Listagem 4.20: Declaração da classe Spi8051 A estrutura que permite fazer a configuração do driver é composta por sete elementos, que permitem configurar o modo, a linha de seleção, a taxa de transferência, o endereço, a operação, e a polaridade e fase do sinal de relógio. A implementação do construtor que permite a configuração do dispositivo SPI é apresentado na listagem Este recebe como parâmetro o apontador da estrutura de configuração. As primeiras três linhas de código permitem inicializar os atributos intrínsecos ao objeto com os respetivos parâmetros da estrutura e configuração. Depois disso, é feita a configuração no hardware através do registo de controlo SPCON. As últimas três linhas de código permitem especificar a linha de seleção e adicioná-la aos atributos da classe. Spi8051::Spi8051(spi8051 Config Config) Address = Config >addr; RW = Config >rw; Mode = Config >mode; SPCON = Config >mode Config >rate Config >clk pol Config >clk phase; Chip Select = (1<<Config >cs); Devices[Num Devices] = Chip Select; Num Devices++; } Listagem 4.21: Construtor da classe Spi8051 com configuração O método da listagem 4.22 implementa a receção de um byte de dados. Para receber a informação é necessário limpar o registo de status e de dados, e esperar que esse registo sinalize a finalização da transferência ou a ocorrência de algum erro. Caso os dados sejam recebidos corretamente, estes são lidos do registo SPDAT e a função retorna true (!0). Caso contrário a função retorna false (0). bool spi8051::read char(unsigned char c) SPSTA=reset; //Clear SPDAT=reset; //Data do }while(spsta!= data t complete && SPSTA!= write collision && SPSTA!= ss slave error && SPSTA!= mode fault); if(spsta == data t complete) c = SPDAT; 107

126 4.2. Upgrade do ADEOS } return true; } else return false; } Listagem 4.22: Métodos read char da classe Spi8051 A implementação dos restantes métodos pode ser consultada no código fonte do driver SPI desenvolvido Upgrade: escalonador power-aware Nos últimos anos, o consumo de energia tem sido uma das principais métricas no projeto e concepção de dispositivos digitais, devido ao aumento crescente na procura de sistemas portáteis como telemóveis, tablets, máquinas fotográficas e dispositivos médicos, onde se pretende minimizar o consumo de energia e simultaneamente maximizar a performance e a complexidade das funcionalidades. O design destes sistemas requer obviamente o uso de processadores reprogramáveis (microcontroladores, microprocessadores, DSPs), que funcionam como o núcleo do sistema. Assim sendo, o constante aumento de funcionalidades dos sistemas tende a ser realizado por software, que é sustentado pela elevada performance dos processadores mais modernos. Por outras palavras, existe um conflito no desenho e concepção destes sistemas: como sistemas portáteis, estes devem ser desenhados para maximizar a duração da bateria; mas, como dispositivos inteligentes, estes necessitam de processadores com elevada capacidade de processamento (que consomem mais energia que os que são usados em dispositivos simplistas), o que se traduz numa redução do tempo útil da bateria. Reconhecendo a necessidade de redução do consumo de energia nos processadores destes dispositivos modernos, a comunidade científica propôs um conjunto de soluções a nível de hardware e software. A nível de software, os métodos propostos podem ser classificados em duas categorias: (i) técnicas de compilação power-aware; (ii) técnicas de gestão do consumo de energia através do sistema operativo. A segunda abordagem tem sido mais explorada, devido ao reconhecimento da importância dos sistemas operativos na gestão do consumo dos componentes do sistema. É neste sentido que surge o escalonador power-aware. Um escalonador poweraware é um escalonador que procura tirar partido das funcionalidades dos processa- 108

127 Capítulo 4. Implementação do Sistema dores mais modernos, de modo a minimizar o consumo de energia (dos processadores), todavia sem comprometer a execução das aplicações. Por outras palavras, um escalonador power-aware implementa ou modifica uma estratégia de escalonamento com base no facto dos processadores mais modernos disponibilizarem diferentes modos de operação, bem como frequência e tensão de operação variáveis. Resumindo, estas estratégias de escalonamento só são implementáveis caso os processadores disponham desses recursos. Como foi mencionado no início do documento, a presente dissertação é apenas uma fração de um trabalho conjunto de hardware-software co-design, que inclui também o desenvolvimento de um microcontrolador de baixo consumo customizável. O microcontrolador será implementado em FPGA, daí que apenas dará suporte a frequências de operação diferentes. Assim sendo, o estratégia de escalonamento a implementar terá de explorar apenas essa característica para minimizar o consumo do microcontrolador, visto que a variação da tensão apenas é possível de implementar em ASIC. Algoritmo de escalonamento power-aware Dos inúmeros trabalhos desenvolvidos na área [72, 73, 74, 75, 76], o autor reconheceu especial interesse ao trabalho desenvolvido por Pillai e Shin [75]. O trabalho desenvolvido pelos investigadores distingue-se dos demais, pois os métodos de redução de energia implementados garantem as deadlines das tarefas, daí poderem ser aplicados em sistemas de tempo-real. Os autores exploram as alterações necessárias a aplicar a escalonadores usados em sistemas operativos de tempo-real, de modo a conseguir reduzir o consumo energético, sem porventura comprometer as deadlines das tarefas. Os métodos implementados são baseados nos algoritmos DVS (Dynamic Voltage Scaling), que diminuem a tensão de operação e a frequência do processador nos momentos em que a carga de processamento é baixa. Neste caso, os investigadores exploram apenas a variação da frequência nos métodos implementados, daí a especial atenção do autor da dissertação para este trabalho. Os três métodos implementados - (i) statically-scaled, (ii) cycle-conversing e (iii) look-ahead - modificam duas estratégias de escalonamento de tempo-real: rate-monotonic e earliest deadline first. O primeiro método (statically-scaled) é estático e consiste na redução da frequência para um valor que garanta as deadlines de um conjunto de tarefas. Para selecionar a frequência apropriada é calculado um fator baseado na frequência máxima de 109

128 4.2. Upgrade do ADEOS operação e a frequência discreta selecionada. A frequência mínima é aceite com base na menor frequência discreta que garanta a deadline das tarefas. A garantia da deadline é testada com base no período e o pior tempo de execução (WCET - worst case execution time) de cada tarefa. Este método não explora completamente a redução da frequência, pois ignora os casos nos quais a tarefa executa menos que o seu WCET. Apesar de não ser o mais agressivo é sem dúvida o mais fácil de implementar, pois os cálculos são realizados estaticamente. Algoritmo 5 Look-Ahead DVS para o escalonador EDF select frequency(x): lowest freq. f i f 1,...,f m f 1 <... <f m } such that x f i /f m upon task release(t i ): set c left i = C i ; defer(); upon task completation(t i ): set c left i = 0; defer(); during task execution(t i ): decrement c left i ; defer(): set U = C 1 /P C n /P n ; set s = 0; for i = 1 to n, T i T 1,...,T n D 1... D n }; set U = U - C i /P i ; set x = max(0, c left i - (1-U)(D i -D n )); set U = U + (c left i -x)/(d i -D n ); set s = s + x; endfor select frequency (s/(d n - current time)); Por sua vez, o método cycle-conserving, contrariamente ao método estático, procura aproveitar os ciclos que sobram das execuções das tarefas anteriores para executar as próximas tarefas a velocidades mais baixas, isto é, com frequências mais baixas. Para isso, cada vez que uma tarefa termina ou é suspensa, o escalonador recalcula a utilização do sistema. Este método é mais agressivo e mais difícil de implementar 110

129 Capítulo 4. Implementação do Sistema que o anterior, pois o cálculo da utilização do processador é feito dinamicamente. O último método, look-ahead, ao contrário dos outros métodos, começa a execução das tarefas a baixa frequência e apenas aumenta a frequência se precisar de garantir as deadlines. Este método aproveita da melhor forma a existência de ciclos mortos, resultantes da execução da tarefa em menos tempo que o WCET. De todos este é o método mais agressivo, e o mais difícil de implementar. Em contrapartida, é o que apresenta melhores resultados, segundo Pillai e Shin, conseguindo atingir reduções de consumo na ordem dos 66% quando comparado com a execução plena do algoritmo EDF. Com base nesses resultados, o autor irá implementar o método look-ahead (algoritmo 5). No pseudo-código f i representa a frequência selecionada entre as frequências discretas disponíveis, f m a frequência máxima, T i representa a tarefa i da lista de tarefas, C i o WCTE da tarefa i, c left i o tempo que falta para atingir o WCET da tarefa i, P i o período da tarefa i, D i a deadline da tarefa i, U a utilização do sistema, e s o número total de ciclos mínimo que é necessário executar antes da deadline mais próxima. Implementação do escalonador power-aware A implementação do método look-ahead com escalonamento EDF para o sistema operativo ADEOS, consistiu basicamente na reimplementação da classe Sched, Task e TaskList. Estas classes foram implementadas com uma nova designação (Sched PW, Task PW e TaskList PW) já a pensar na customização e configuração do sistema operativo. Na definição da classe Task PW (listagem 4.23), foram acrescentadas alguns atributos, cuja informação se torna essencial para o método look-ahead, nomeadamente o período, deadline e WCET da tarefa. No construtor é também calculada a utilização global do sistema (U) para determinar se o conjunto de tarefas é escalonável ou não. Assim sendo, antes de adicionar a tarefa, é verificado se o é possível fazer. Caso não o seja, o construtor da classe retorna sem adicionar a tarefa á lista de tarefas. Task PW::Task PW(void ( function)(), int stacksize, Deadline Task deadline, WCET Task wcet) stacksize /= sizeof(int); //Power Aware: global Utilization test int temputili = os.globutiliz + ((float)task wcet/(float)task deadline ) 100; if(temputili >=100) return; //If U > 100% return and dont insert this task else 111

130 4.2. Upgrade do ADEOS if(!(function == idle)) //If Not idle, update GlobalUtilization os.globutiliz = temputili; } } } entercs(); ////// Critical Section Begin // Initialize the task specific data.... period = Task deadline;//poweraware deadline = Task deadline;//poweraware wcet = Task wcet; //PowerAware cleft = wcet; //PowerAware... exitcs(); ////// Critical Section End Listagem 4.23: Construtor da classe do escalonador power-aware Na classe TaskList PW foi necessário reimplementar os métodos de inserção e remoção das tarefas. Isto porque foi necessário implementar uma lista duplamente ligada, uma vez que o método look-ahead necessita de percorrer a lista de tarefas nos dois sentidos. Por sua vez, na classe Sched PW, para além das modificações ao nível da classe, foram também modificadas a rotina da interrupção de overflow do temporizador 1, bem como o método schedule. Além disso, foram introduzidos os métodos defer e select freq essenciais para a implementação do algoritmo look-ahead especificado anteriormente. Na rotina de interrupção de overflow do temporizador 1 (listagem 4.24), responsável pelo clock-tick do escalonador, são então atualizadas as variáveis responsáveis pelo tempo total decorrido no sistema (os.currenttime), assim como o tempo que falta para a conclusão do WCET da tarefa (os.prunningtask->cleft). #pragma vector = TF1 int interrupt void Sched::tick(void) entercs(); recharge sched tick( os.cycles tick); os.prunningtask >cleft =os.tick;//power Aware os.currenttime+=os.tick;//power Aware os.schedule(); exitcs(); } Listagem 4.24: Alterações na ISR do clock-tick do escalonador 112

131 Capítulo 4. Implementação do Sistema No método schedule, é então introduzida a chamada do método defer, responsável por determinar se é possível baixar a frequência de relógio do CPU sem comprometer as deadlines das tarefas. Além disso, caso a próxima tarefa a entrar em execução seja a idle, então a frequência é baixada para o mínimo e os atributos do escalonador que permitem gerir as temporizações dinamicamente são atualizados (os.f cpu e os.cycles tick). A implementação da função defer é apresentada na listagem Aqui não há muito a explicar porque consiste na tradução fidedigna do pseudo-código apresentado no algoritmo look-ahead. void Sched PW::defer(void)... // Look Ahead Algorithm do Utilization= Utilization + (((float)pprev >wcet/(float)pprev >period) 100); pprev = pprev >pnext; }while (pprev!= &os.idletask && readylist.ptop!= &os.idletask); pprev = pprev >pprevious;//idle >pprevious s = 0; while(pprev!= NULL) long temp = (((float)pprev >wcet/(float)pprev >period) 100); Utilization = Utilization temp; temp = (long)(pprev >cleft (long)(100 Utilization) (long)(pprev >deadline readylist.ptop >deadline)); if(temp<0) x = 0; else x= temp; Utilization = Utilization + (pprev >cleft x)/ (pprev >deadline readylist.ptop >deadline); s=s+x; pprev = pprev >pprevious; } SelectFreq(((unsigned int)(s 16)/(unsigned int)(readylist.ptop >deadline currenttime)) 100); } Listagem 4.25: Implementação do método defer Finalmente, a tabela 4.4 apresenta o código C++ e assembly da implementação do método de selecção de frequência, invocado no final do método defer. Esta função para além de invocar a função implementada em assembly de seleção de frequência, atualiza também os atributos que permitem gerir a temporização dinamicamente. A implementação em assembly atualiza os registos do microcontrolador customizável dedicados ao escalonador. 113

132 4.3. Refactoring do ADEOS Tabela 4.4: Implementação C++ e assembly da seleção da frequência Método C++ CDP (8051) void Sched PW::SelectFreq(unsigned int x) os.f cpu = F max>>select freq(x); } os.cycles tick = (((F cpu) os.tick)/12); 4.3 Refactoring do ADEOS select freq: CODE ;HWFSH = ((unsigned char)x >> 8) 0xC0; MOV A,R5 ORL A,#0xC0 MOV HWFSH,A ;HWFSL = (unsigned char)x; MOV A,R4 MOV HWFSL,A flag freq: ;while ( HWFSH & (1<<7)) MOV A,HWFSH MOV C,A.7 JC flag freq ; CKRL = ((HWFSH >> 3) & 0x7) MOV A,HWFSH RR A RR A RR A ANL A,#0x07 MOV CKRL,A ;return CKRL in R1 MOV R1,CKRL RET A terceira e última parte do desenvolvimento do sistema, é a fração fundamental do problema da dissertação. Basicamente, consiste na reestruturação ou refactoring do sistema operativo ADEOS, aplicando a técnica de programação template metaprogramming. Desta forma, é possível gerir a variabilidade das funcionalidades e permitir a customização do sistema operativo, sem comprometer o desempenho e introduzir overhead de memória Diagrama de Funcionalidades O diagrama de funcionalidades é uma representação visual do modelo de funcionalidades. Este modelo surgiu com o conceito da orientação a funcionalidades [77], permitindo a gestão das funcionalidades comuns e variáveis de um sistema em linha de produção, sem ter em conta o mecanismo de implementação a utilizar. O diagrama de funcionalidades representa um conjunto de funcionalidades, organizadas hierarquicamente, onde o nodo da raiz representa o conceito do sistema e os nodos descendentes as funcionalidades [7]. Este contém quatro tipos possíveis de funcionalidades: Funcionalidades obrigatórias: O sistema deve ter obrigatoriamente certas 114

133 Capítulo 4. Implementação do Sistema funcionalidades. Estas funcionalidades são representadas com um círculo preenchido a preto. Funcionalidades opcionais: O sistema pode, ou não, ter certas funcionalidades. Estas funcionalidades são representadas com um círculo sem preenchimento. Funcionalidades alternativas: O sistema apenas tem uma funcionalidade em cada instante de tempo. Estas funcionalidades são representadas com um arco sem preenchimento. Funcionalidades combinadas: O sistema pode ter uma combinação de funcionalidades. Estas funcionalidades são representadas com um arco preenchido a preto. Figura 4.11: Diagrama de funcionalidades do ADEOS A figura 4.11 apresenta o diagrama de funcionalidades do sistema operativo ADEOS. O nó raiz representa o conceito (ADEOS) que é composto por quatro funcionalidades: Task, IPC, Driver e Scheduler. As funcionalidades apresentadas correspondem aos componentes do sistema operativo. A funcionalidade Task tem cardinalidade [1..*], o 115

134 4.3. Refactoring do ADEOS que significa que o ADEOS tem que ser composto no mínimo por uma tarefa (idle). No entanto, pode ter outras tarefas, consoante as necessidades do utilizador. A funcionalidade Scheduler tem cardinalidade [1], ou seja, é obrigatório a presença de um, e apenas um, escalonador no núcleo do sistema. As funcionalidades IPC e Driver têm cardinalidade [0..*], que indica que estas funcionalidades são opcionais. Por exemplo, só é necessário ter a funcionalidade Driver caso seja necessário comunicar com algum periférico. Da mesma forma, só é necessário a funcionalidade IPC, caso se pretenda ter comunicação entre as tarefas. A funcionalidade Task tem variabilidade. Por exemplo, uma tarefa pode ser caracterizada pela prioridade, caso a intenção seja utilizar a estratégia de escalonamento (highest priority first), ou então pela sua deadline, caso se pretenda utilizar o algoritmo earliest deadline first. Neste sentido, é possível ter tantos gestores de tarefas quantos os desejados, todavia mutuamente exclusivos. Apenas um deles pode ser usado em cada configuração. A funcionalidade IPC é constituída por tantas funcionalidades cumulativas quantas as pretendidas. Como exemplo apresenta-se os mecanismos semaphores e mutex, mas também podem ser utilizados message queue e shared memory. Estas funcionalidades são cumulativas, porque podem ser utilizadas todas ao mesmo tempo, de forma combinada, ou até podem não ser utilizadas. Cada uma das funcionalidades também apresenta variabilidade. No entanto as subfuncionalidades são exclusivas. Quer isto dizer que o sistema operativo ADEOS pode utilizar, por exemplo, o mecanismo de semaphore e mutex ao mesmo tempo, no entanto só pode utilizar uma implementação de cada mecanismo em cada configuração. A funcionalidade Driver é semelhante à funcionalidade anterior. Desta forma, podem existir tantos drivers quantos os periféricos com quem se pretende comunicar. Porta-série, I 2 C, bem como SPI, PWM, são tudo funcionalidades cumulativas que podem ser utilizadas ao mesmo tempo, mas com implementações exclusivas. Ou seja, as funcionalidades são cumulativas, no entanto a variabilidade dentro delas (subfuncionalidades) é exclusiva. Finalmente, a funcionalidade Scheduler é semelhante à funcionalidade Task. Isto significa que o sistema operativo pode ter diferentes implementações do escalonador, no entanto mutuamente exclusivas. 116

135 Capítulo 4. Implementação do Sistema Estratégia de Gestão da Variabilidade Conforme foi visto na secção 3.3 a técnica de template metaprogramming não é intuitiva e a sintaxe é por vezes um pouco isotérica. Neste sentido, para gerir a variabilidade do sistema operativo, e consequentemente as diversas funcionalidades, é necessário definir uma metodologia que sistematize a restruturação de cada uma. Assim, como foi visto anteriormente, a variabilidade dentro de cada funcionalidade especifica é mutuamente exclusiva, o que significa que, por exemplo, se for definido o driver usart1 na configuração, implica que não pode ser usado mais nenhum. Assim sendo, a metodologia de gestão da variabilidade de uma funcionalidade com template metaprogramming completa-se em três etapas. Tabela 4.5: Classes especificas da funcionalidade example example1.h class example1 public: example1() } //Constructor example1() } //Destructor void func(); void set attr(unsigned char); unsigned char get attr(); private: unsigned char attr 1; }; //Method example void example1::func() } //Attribute set example void example1::set attr(unsigned char attr) attr 1 = attr; } //Attribute get example unsigned char example1::get attr() return attr 1; } example2.h class example2 public: example2() } //Constructor example2() } //Destructor void func(); void set attr(unsigned int); unsigned int get attr(); private: unsigned int attr 2; }; //Method example void example2::func() } //Attribute set example void example2::set attr(unsigned int attr) attr 2 = attr; } //Attribute get example unsigned int example2::get attr() return attr 2; } A primeira etapa consiste na divisão de cada uma das implementações da funcionalidade em tantos ficheiros cabeçalhos quantas as implementações. Supondo que o sistema operativo inclui a funcionalidade example, com variabilidade exclusiva a dois níveis, isto é, ou é utilizada a implementação example1 ou então a implementação example2. Assim, a primeira etapa consiste então em definir cada uma das clas- 117

136 4.3. Refactoring do ADEOS ses que implementa cada uma das funcionalidades específicas, em diferentes ficheiros cabeçalho. A tabela 4.5 apresenta a definição de cada uma dessas hipotéticas classes. Estas classes servem apenas para explicar a estratégia que deve ser utilizada, não implementando portanto qualquer funcionalidade. Para além do construtor e destrutor da classe, implementam um método genérico, bem como os métodos set e get de um atributo. Importa salientar que os atributos têm tipos diferentes, para ilustrar a possibilidade de o utilizar. Por sua vez, na segunda etapa é definido um ficheiro cabeçalho (* tmp.h) onde é feita então a implementação da funcionalidade com template metaprogramming. Basicamente, consiste em definir o protótipo da template e a funcionalidade especifica a utilizar. Depois disso é implementada a template genérica, bem como cada uma das templates específicas (example1 e example2 ). O código 4.26 apresenta o ficheiro example tmp.h, que corresponde à implementação com template metaprogramming da funcionalidade example. #include example1.h #include example2.h template <typename exampletype> class examplemanager; //Specify Template Prototype typedef example1 example; //Specify Specific Template typedef examplemanager<example> Example; example example object; //Define object //Generic Template template <> class examplemanager <examplegeneric> public: inline static void func() return; / error /} inline static void set attr() return; / error /} inline static void get attr() return; / error /} }; //Specific Template 1 template <> class examplemanager <example1> public: inline static void func() example object.func(); } inline static void set attr(unsigned char attr) example object.set attr(attr); } 118

137 Capítulo 4. Implementação do Sistema }; inline static unsigned char get attr() return example object.get attr(); } //Specific Template 2 template <> class examplemanager <example2> public: inline static void func() example object.func(); } inline static void set attr(unsigned int attr) example object.set attr(attr); } inline static unsigned int get attr() return example object.get attr(); } }; Listagem 4.26: Ficheiro example tmp.h Finalmente, a última etapa consiste em utilizar a funcionalidade com a abstração necessária independentemente da funcionalidade especifica. Quer isto dizer, que o código produzido que utiliza a funcionalidade não deve ser diferente independentemente da funcionalidade especificada na configuração pretendida. Seguindo o exemplo da funcionalidade example, o código da listagem 4.27 permite aceder tanto aos métodos da classe example1 como da classe example2. A escolha é feita exclusivamente no ficheiro example tmp.h na linha typedef examplex example. Substituindo X por 1 ou por 2 é possível definir a configuração pretendida. Todavia, o código que usa a funcionalidade é exatamente o mesmo.... int var = 0; Example ex; ex.func(); ex.set attr(0x12); var = ex.get attr();... Listagem 4.27: Transparência no código de acesso à funcionalidade example Esta metodologia sistematiza então a estratégia de implementação das diversas 119

138 4.3. Refactoring do ADEOS funcionalidades com TMP. No exemplo anterior, apenas foi tratada a variabilidade a dois níveis. No entanto, caso a variabilidade fosse a três níveis, a metodologia era a mesma. Simplesmente bastava definir mais um ficheiro cabeçalho (example3.h) com a implementação da classe pretendida, e no ficheiro example tmp.h especificar a template especifica para esse caso. O código que usa a funcionalidade permanece o mesmo, e é otimizado para a configuração escolhida no ficheiro example tmp.h, não incluindo portanto o código das implementações excluídas Reestruturação do ADEOS Na reestruturação do sistema operativo ADEOS para permitir a gestão da variabilidade das funcionalidades, o autor centra-se mais em implementar o suporte à variabilidade do que a própria variabilidade. Nesse sentido, é normal que a variabilidade dentro de uma funcionalidade apareça replicada, pois o importante é aplicar a metodologia explicada anteriormente a cada uma das funcionalidades do ADEOS. São reestruturadas as funcionalidades Sched, Task, Mutex, bem como todos os device drivers desenvolvidos. No entanto, o autor decidiu explicar apenas duas: a gestão do escalonador e a gestão das tarefas. Isto porque apesar destas funcionalidades seguirem todas a mesma estratégia, apresentam pequenas variantes. Todas as outras funcionalidades que não são apresentadas, são reestruturadas de forma análoga, podendo os detalhes da implementação serem consultados no código do sistema operativo configurável. Escalonador com Template Metaprogramming Tabela 4.6: Declaração da classe template da funcionalidade Sched sched tmp.h #include sched1.h #include sched2.h template <typename SchedType> class schedmanager; typedef schedmanager<sched> Sched; sched sched obj;... config adeos.h... / sched tmp / #define sched Sched1... A reestruturação do escalonador para permitir a sua customização consiste exatamente na aplicação da metodologia explicada anteriormente. Isto porque no sistema 120

139 Capítulo 4. Implementação do Sistema Tabela 4.7: Definição das templates genérica e especificas da funcionalidade Sched Template Generic Sched1 Sched2 Código Template template <> class schedmanager <SchedGeneric> public: inline static void start() return; / error /} inline static void schedule() return; / error /} inline static void add Task(Task ptask) return; / error /} inline static void enterisr() return; / error /} inline static void exitisr() return; / error /} inline static void get prunningtask() return; / error /} inline static void get pidletask() return; / error /} inline static void get preadylist() return; / error /} }; template <> class schedmanager <Sched1> public: inline static void start() sched obj.start();} inline static void schedule() sched obj.schedule();} inline static void add Task(Task ptask) sched obj.add Task(pTask);} inline static void enterisr() sched obj.enterisr();} inline static void exitisr() sched obj.exitisr();} inline static Task get prunningtask() return sched obj.prunningtask;} inline static Task get pidletask() return &sched obj.idletask;} inline static ReadyList get preadylist() return &sched obj.readylist;} }; template <> class schedmanager <Sched2> public: inline static void start() sched obj.start();} inline static void schedule() sched obj.schedule();} inline static void add Task(Task ptask) sched obj.add Task(pTask);} inline static void enterisr() sched obj.enterisr();} inline static void exitisr() sched obj.exitisr();} inline static Task get prunningtask() return sched obj.prunningtask;} inline static Task get pidletask() return &sched obj.idletask;} inline static ReadyList get preadylist() return &sched obj.readylist;} }; operativo existe apenas uma instância dessa funcionalidade. Assim sendo, em primeiro lugar cada uma das classes que implementa o escalonador deve ser definida num ficheiro cabeçalho. Como o autor implementa duas estratégias de escalonamento diferentes, haverá dois ficheiros cabeçalhos. No entanto, caso surjam novas implementações, será necessário criar tantos ficheiros cabeçalhos quantas as novas implementações. Depois disso, é criado o ficheiro cabeçalho sched tmp.h, responsável por fazer a gestão da funcionalidade estaticamente com template metaprogramming. No inicio do ficheiro é feita a inclusão a todos os ficheiros cabeçalhos que implementam os algoritmos de escalonamento, e é definido o protótipo da template. Depois disso, simplifica-se a sintaxe isotérica das templates, e é definida uma instância de 121

140 4.3. Refactoring do ADEOS um objeto do tipo escalonador. No ficheiro cabeçalho config adeos.h configura-se o algoritmo especifico de escalonamento a utilizar. A tabela 4.6 apresenta o código do que foi descrito. O próximo passo consiste na especificação da template genérica, e de cada uma das templates específicas para cada algoritmo de escalonamento. Basicamente, consiste em definir uma classe template que tem métodos comuns a toda a funcionalidade do escalonador, mas que são substituídos pelos métodos específicos da estratégia de escalonamento configurada. O facto dos métodos serem inline, significa que no local onde são utilizados são substituídos pelo código da implementação, evitando um salto adicional. A tabela 4.7 resume essa implementação. Desta forma, o código transparente que gere a funcionalidade é sempre o mesmo, pois todas as classes templates tem a mesma especificação. No entanto, a implementação dos métodos de cada template é que é diferente. Contudo, como o código genérico é substituído apenas pelo código específico da template configurada, garantese assim que apenas a funcionalidade pretendida é incorporada, gerando código otimizado de acordo com a configuração. A listagem 4.28 ilustra como o código da funcionalidade Sched permanece transparente, apesar da inclusão da variabilidade na funcionalidade. Sched os;... void main(void) os.add Task(os.get pidletask()); os.add Task(&taskA); os.add Task(&taskB); os.start(); } Listagem 4.28: Transparência no código de acesso à funcionalidade Sched Tarefas com Template Metaprogramming O refactoring do código relativo à funcionalidade Task segue a mesma metodologia até agora apresentada, no entanto com umas ligeiras modificações. Isto porque a estratégia apresentada funciona corretamente quando existe apenas um objeto da classe específica da funcionalidade. No entanto, no caso da funcionalidade Task isso não acontece. Primeiro porque o sistema operativo pode executar várias tarefas (várias instâncias da classe Taskx), e segundo porque para gerir as tarefas este é 122

141 Capítulo 4. Implementação do Sistema composto por várias listas de tarefas (várias instâncias da classe TaskListx). Uma lista de tarefas é responsável por reter as tarefas prontas a executar (readylist), enquanto outras listas estão associadas a cada mutex responsável por remover a tarefa da lista de tarefas e colocá-la na waitlist. Como cada objeto do tipo mutex tem associado uma waitlist, então haverá tantas listas quantos os mutex. Neste sentido, é necessário modificar a metodologia até agora utilizada, de modo a suportar diferentes e múltiplas instâncias da mesma classe. No caso da gestão da lista de tarefas, é necessário um objeto do tipo readylist e tantos objetos do tipo waitlist quantos os mutex (tarefas) utilizados. A solução encontrada passa então por utilizar um meta-argumento na definição da template da classe. Este meta-argumento permite distinguir uma readylist duma waitlist. Por sua vez, para distinguir cada uma das waitlist, é utilizado um atributo (id) na chamada dos métodos associados. Este atributo é intrínseco a cada mutex, e incrementado a cada nova instanciação. Tabela 4.8: Declaração da classe template da funcionalidade Task task tmp.h #include task1.h #include task2.h template <unsigned char n, typename tasktype> class taskmanager; typedef taskmanager<0,tasklist> ReadyList; typedef taskmanager<1,tasklist> WaitList; TaskList readylist, waitlist[num waitlist];... config adeos.h... / task tmp / #define num waitlist 3 #define Task Task1 #define TaskList TaskList1... Explicando concretamente a reestruturação do código da funcionalidade Task, a primeira parte consiste então na definição de tantos ficheiros cabeçalho tantas as especificações. O autor implementa a variabilidade a dois níveis, daí haver dois ficheiros cabeçalhos (task1.h e task2.h). Depois disso, é criado o ficheiro cabeçalho task tmp.h, responsável por fazer a gestão da funcionalidade estaticamente com template metaprogramming. No início do ficheiro (tabela 4.8) é feita a inclusão a todos os ficheiros cabeçalhos, e é definido o protótipo da template. De notar a utilização do meta-argumento n, do tipo unsigned char, que permite especificar 256 variantes da mesma lista. A utilização da keyword typedef no código permite simplificar a sintaxe na designação atribuída à ReadyList e WaitList. A última linha de código define um objeto do tipo ReadyList e tantos objetos do tipo WaitList quantos os especificados no ficheiro de configuração. Nesse ficheiro também se define qual a funcionalidade especifica das tarefas a utilizar. 123

142 4.3. Refactoring do ADEOS De seguida são especificadas as templates genérica e específicas de cada uma das implementações (tabela 4.9). Basicamente, consiste em definir uma classe template que tem métodos comuns a toda a funcionalidade das tarefas, mas que são substituídos pelos métodos específicos da classe configurada (no ficheiro config adeos.h). Aqui importa justificar o porquê de implementar métodos overloading. Esta foi a forma mais simples de implementar a existência de diferentes objetos. Como existe apenas uma lista ReadyList, então não é preciso identificar qual delas é. Daí que os métodos sejam implementados sem a utilização do argumento id. Por outro lado, como existem vários objetos do tipo WaitList, então é necessário implementar os mesmos métodos, mas com o argumento extra de identificação da lista. Daí ser utilizado o argumento id. Tal como foi referido este argumento utilizado no método é um atributo intrínseco de cada objeto mutex, que permite identificar no array de objetos WaitList, a respetiva lista associada ao mutex. Por isso é comum utilizar waitlist[id ] na implementação dos métodos da template. Com esta abordagem, o código do sistema operativo que gere esta funcionalidade permanece praticamente o mesmo (listagem 4.29), isto é, semelhante ao código do sistema operativo sem variabilidade, apenas nos métodos da waitlist é necessário especificar o id do mutex. Além disso o código é suficientemente transparente e abstracto para que alterando a configuração da funcionalidade, não seja necessário modificar esse código que a gere.... ReadyList readylist; readylist.insert(ptask); readylist.set ptop(null); readylist.get ptop();... WaitList waitinglist; waitinglist.insert(pcallingtask,this >id); waitinglist.set ptop(null,this >id); waitinglist.get ptop(this >id)... Listagem 4.29: Transparência no código de acesso à funcionalidade Task 124

143 Capítulo 4. Implementação do Sistema Tabela 4.9: Definição das templates genérica e especificas da funcionalidade Task Template Generic Task1 Task2 Código Template template <unsigned char n> class taskmanager <n, TaskListGeneric> public: inline static void insert(task ptask) return; / error /} inline static void insert(task ptask, unsigned char id ) return; / error /} inline static void remove(task ptask) return; / error /} inline static void remove(task ptask, unsigned char id ) return; / error /} inline static void get ptop() return; / error /} inline static void get ptop(unsigned char id ) return; / error /} inline static void set ptop(task ptask) return; / error /} inline static void set ptop(task ptask, unsigned char id ) return; / error /} }; template <> class taskmanager <0, TaskList1> public: inline static void insert(task ptask) readylist.insert(ptask); } inline static Task remove(task ptask) return readylist.remove(ptask); } inline static Task get ptop() return readylist.ptop; } inline static void set ptop(task ptask) readylist.ptop = ptask; } }; template <> class taskmanager <1, TaskList1> public: inline static void insert(task ptask, unsigned char id ) waitlist[id ].insert(ptask); } inline static Task remove(task ptask, unsigned char id ) return waitlist[id ].remove(ptask); } inline static Task get ptop(unsigned char id ) return waitlist[id ].ptop; } inline static void set ptop(task ptask, unsigned char id ) waitlist[id ].ptop = ptask; } }; template <> class taskmanager <0, TaskList2> public: inline static void insert(task ptask) readylist.insert(ptask); } inline static Task remove(task ptask) return readylist.remove(ptask); } inline static Task get ptop() return readylist.ptop; } inline static void set ptop(task ptask) readylist.ptop = ptask; } }; template <> class taskmanager <1, TaskList2> public: inline static void insert(task ptask, unsigned char id ) waitlist[id ].insert(ptask); } inline static Task remove(task ptask, unsigned char id ) return waitlist[id ].remove(ptask); } inline static Task get ptop(unsigned char id ) return waitlist[id ].ptop; } inline static void set ptop(task ptask, unsigned char id ) waitlist[id ].ptop = ptask; } }; 125

144

145 Capítulo 5 Resultados Experimentais No capítulo anterior foi apresentada a implementação do sistema, começando pelo porting do ADEOS para a arquitetura MCS-51, seguindo-se o upgrade e refactoring do sistema operativo. A reestruturação do ADEOS para a gestão da variabilidade foi conseguida utilizando a técnica de template metaprogramming. Neste capítulo, são apresentados os resultados experimentais dos testes realizados, numa placa de desenvolvimento com o microcontrolador da família 8051 da Atmel, para avaliar o desempenho e overhead de memória, bem como as métricas de gestão do código. Foram efetuados dois testes distintos. No primeiro, o sistema operativo e as diversas funcionalidades foram implementadas de duas formas diferentes: a implementação na linguagem C++ onde é utilizado template metaprogramming para gerir da variabilidade; e a implementação na linguagem C++ onde é utilizado polimorfismo dinâmico para gerir a variabilidade do sistema operativo. Por sua vez, no segundo teste, apenas foi averiguado um módulo de device driver. Isto porque para além das duas implementações em C++, surge uma terceira implementação em C utilizando compilação condicional. 5.1 Ambiente de Testes Caracterizar o ambiente em que decorreram os testes realizados implica caracterizar essencialmente três componentes: o hardware onde os testes foram realizados; o compilador usado para compilar o código fonte dos testes realizados; e as ferramentas de software para avaliação das métricas em teste. Para acelerar o desenvolvimento e avaliar o sistema operativo customizável no 127

146 5.1. Ambiente de Testes Tabela 5.1: Caracterı sticas de hardware da placa de desenvolvimento 8051DKUSB Placa de desenvolvimento 8051DKUSB Caracterı sticas Arquitetura: 8051 Processador: AT89C51ID2 Velocidade CPU: 12MHz RAM: 256-bytes XRAM: 1792-bytes Flash: 64-kbytes EEPROM: 2048-bytes desempenho e footprint de memo ria, sem depender do trabalho de terceiros, os testes foram realizados na plataforma de hardware 8051DKUSB (figura 5.1). Esta placa de desenvolvimento, desenvolvida in-house (ESRG), vem equipada com um microcontrolador AT89C51ID2 da Atmel, alimentac a o USB, conector de 44 pinos para expansa o dos quatro portos do microcontrolador, comunicac a o se rie atrave s da porta USB (FTDI), display de 7-segmentos ligado ao porto 1, e programac a o ISP (InSystem Programming) manual ou automa tica. A tabela 5.1 resume as caracterı sticas fundamentais do microcontrolador. Figura 5.1: Placa de desenvolvimento 8051DKUSB Para compilar o co digo fonte do sistema operativo ADEOS, incluindo o co digo das tarefas a executar, foi utilizado o compilador C/C++ da IAR para o Nas opc o es de compilac a o foi definida a opc a o de otimizac a o None, de modo a obter co digo ma quina sem qualquer otimizac a o. Desta forma sera possı vel avaliar de forma mais fidedigna da influe ncia do template metprogramming nas me tricas de desempenho, 128

147 Capítulo 5. Resultados Experimentais sem grande interferência do compilador. Para obter os resultados das métricas pretendidas foram utilizados essencialmente três utilitários. Para obter os resultados relacionados com o desempenho e memória, foram utilizados o debugger do IAR Embedded Workbench for 8051 e o Flip da Atmel, respetivamente. Por sua vez, para obter os resultados relacionados com as métricas de gestão do código foi utilizado o software Understand da Scientific Toolworkss [78]. 5.2 Métricas de Teste Na secção 2.3 o autor justificou a escolha da técnica de template metaprogramming como a solução adequada para gerir a variabilidade do sistema operativo implementado com o paradigma da programação orientada a objetos. Isto porque apesar do overhead associado a algumas características desse paradigma de programação, a técnica de template metaprogramming permite reestruturar o software de forma a gerir a variabilidade do mesmo, sem porventura comprometer o desempenho e memória do sistema. Assim sendo, faz todo sentido que as métricas em teste estejam relacionadas essencialmente com o tempo de execução (desempenho) e o tamanho do ficheiro de código (memória). No entanto, apesar das métricas desempenho e memória serem fatores preponderantes no projeto e concepção de qualquer sistema, a facilidade de gestão e expansão do código também desempenha um papel importante. Isto porque código ilegível e mal organizado requer um esforço de engenharia superior. Portanto, para verificar o grau de complexidade inerente à gestão do código bem como a sua expansão são analisadas as seguintes métricas: Linhas de Código (LOC): número de linhas de código, excluindo comentários e linhas em branco, presentes nos ficheiros de código fonte; Número de Classes (NOC): número de classes presentes nos ficheiros de código fonte. 5.3 Testes Realizados Como o sistema operativo pode ser configurado de tantas formas quantas as funcionalidades disponíveis, então a realização dos testes e recolha de resultados torna-se 129

148 5.3. Testes Realizados uma tarefa complexa. Isto devido ao aumento substancial de configurações a cada introdução de uma nova funcionalidade. Para simplificar essa tarefa, o autor decidiu realizar um primeiro teste, limitando a variabilidade de cada funcionalidade a dois níveis. Por outras palavras, apenas com a variabilidade a dois níveis, o sistema permite 32 (variabilidade funcionalidades = 2 5 ) configurações. A figura 5.2 ilustra o diagrama de funcionalidades do teste em causa. Para este teste, o autor implementou o sistema utilizando duas metodologias diferentes: (i) a implementação na linguagem C++ onde é utilizado template metaprogramming; e (ii) a implementação na linguagem C++ onde é utilizado polimorfismo dinâmico. Isto para tentar sustentar a premissa de que é possível utilizar a programação orientada a objetos e template metaprogramming para implementar software customizável em sistemas embebidos, pois a maioria das funcionalidades da POO (com exceção do polimorfismo dinâmico, múltipla herança e abstração) não compromete o desempenho do sistema, e facilita a gestão do código. Figura 5.2: Diagrama de funcionalidades do sistema operativo (teste ao sistema operativo) 130

149 Capítulo 5. Resultados Experimentais Tabela 5.2: Configuração usada no teste ao sistema operativo Funcionalidade Sched Task IPC - Mutex Driver - USART Driver - SPI Implementação Sched HPF Task HPF Mutex1 USART AT89C51 SPI AT89C51 Contudo, uma vez que o primeiro teste apenas permite fazer uma comparação entre duas implementações que utilizam programação orientada a objetos, somente com isso não é possível perceber concretamente qual o potencial de otimização da técnica de template metaprogramming, quando comparada com uma implementação imperativa como linguagem C. No entanto, implementar todo o sistema operativo assim como as diversas funcionalidades em linguagem C, seria para o autor uma tarefa inexequível. Por este motivo, o segundo teste centra-se apenas numa funcionalidade de um driver, ou seja, são comparadas e avaliadas as duas implementações em C++ bem como uma implementação em C do device driver UART (também com variabilidade a dois níveis). A implementação em linguagem C utiliza compilação condicional. Também poderia ser implementada utilizando apontadores para funções, no entanto esta metodologia não é tão otimizada quanto a anterior. Com este teste, é então possível estabelecer um ponto de comparação (embora pequeno) entre a implementação C++ TMP e a implementação C otimizada Teste ao Sistema Operativo Com base no diagrama de funcionalidades da figura 5.2, foi possível definir a configuração do sistema operativo (tabela 5.2) para a realização do teste. A configuração implementa um sistema operativo baseado em prioridades, e utiliza a implementação dos drivers UART e SPI na variante Atmel (AT89C51). O teste consiste na execução de duas tarefas periódicas: (i) envio de um caracter via série; e (ii) comunicação com um dispositivo SPI slave. O envio do carácter (tarefa de maior prioridade) é feito a cada dois segundos, enquanto a comunicação com o dispositivo slave é feita a cada cinco segundos. O dispositivo SPI slave está implementado numa placa de circuito impresso (PCB) concebida pelo autor para avaliar e testar os drivers SPI e I 2 C desenvolvidos (apêndice A). 131

150 5.3. Testes Realizados Resultados de Desempenho e Footprint de Memória Os resultados de desempenho traduzem os resultados a nível de tempo de execução. Estes indicam os ciclos de relógio necessários para executar o teste com cada uma das implementações - C++ template metaprogramming e C++ polimorfismo dinâmico. Os resultados de footprint de memória indicam qual a memória de código necessária para executar o teste com cada uma das implementações. Para obter os tempos de execução de cada uma das implementações do sistema operativo, foi utilizado o debugger do ambiente de desenvolvimento. Nessa avaliação não foi considerado o tempo que demora efetivamente a enviar o carácter via série, nem o tempo que demora a enviar a trama I 2 C. Por outras palavras, como os drivers foram implementados utilizando o mecanismo de polling, significa dizer que ao efetuar a depuração a condição de verificação da flag que indica fim de transmissão foi desprezada. Por outro lado, para avaliar o tamanho da memória de código foi utilizado o ficheiro de código produzido para executar na plataforma de teste. (a) Tempo de execução (b) Memória de código Figura 5.3: Resultados de desempenho e footprint de memória (teste ao sistema operativo) Os gráficos da figura 5.3 apresentam os resultados do tempo de execução e memória de código das implementações C++ com template metaprogramming (C++ TMP) e polimorfismo dinâmico (C++ PD) do sistema operativo, para a execução das tarefas anteriormente descritas. Tal como os gráficos ilustram, a implementação com TMP apresenta tanto um 132

151 Capítulo 5. Resultados Experimentais tempo de execução como dimensão de memória de código inferior a outra implementação. Basicamente, a implementação com template metaprogramming reduz cerca de 20% o tempo de execução e 40% a memória de código, relativamente a implementação com polimorfismo dinâmico. Isto deve-se ao facto do código TMP ser otimizado para a configuração pretendida, enquanto na implementação com polimorfismo dinâmico o código é compilado com todas as funcionalidades selecionadas. Isto afecta a linearidade do código, devido ao elevado número de saltos (jumps), consequentes do elevado número de instruções que não são utilizadas, produzindo um impacto negativo na performance do sistema. Os resultados apresentados traduzem apenas dois graus de variabilidade em cada uma das funcionalidades. Experiências realizadas pelo autor com três graus de variabilidade indicam que a implementação TMP pode reduzir o tempo de execução em cerca de 25% e a memória de código em cerca de 50%. Resumindo, num sistema altamente configurável com elevado grau de variabilidade, a otimização usando a técnica de template metaprogramming permite atingir resultados significativos nas métricas em causa. Resultados de Gestão do Código Embora as métricas de desempenho do sistema sejam de especial importância em tempo de execução, não implica que a forma como é feita a gestão e manutenção da variabilidade do código não tenha que ser tida em conta. Assim, torna-se também importante avaliar e comparar as duas implementações do sistema operativo ao nível da gestão do código, nomeadamente, na métricas LOC e NOC. Os gráficos da figura 5.4 apresentam os valores das métricas LOC e NOC para as implementações C++ com template metaprogramming (C++ TMP) e com polimorfismo dinâmico (C++ PD) do sistema operativo. Dos gráficos da figura 5.4, conclui-se que o número de linhas de linhas de código (LOC) das duas implementações é praticamente o mesmo (ligeira superioridade para a implementação com TMP). No que diz respeito a métrica relacionado com o número de classes (NOC), a implementação C++ com template metaprogramming apresenta um valor superior ao da implementação C++ com polimorfismo dinâmico. Isto indica que o código é mais modular e apresenta um nível de encapsulamento superior. Como consequência, torna-se mais fácil fazer a sua gestão, manutenção e possível reutilização. 133

152 5.3. Testes Realizados (a) Número de linhas de código (b) Número de classes Figura 5.4: Resultados de gestão do código (teste ao sistema operativo) Teste ao driver USART Como o teste anteriormente apresentado permite apenas fazer uma comparação entre duas implementações que utilizam programação orientada a objetos, por si só esse teste não permite aferir o potencial de otimização da técnica de template metaprogramming quando comparada com uma implementação em linguagem C. Nesse sentido, o autor decidiu focar-se apenas num módulo e implementar a variabilidade desse módulo com compilação condicional. Isto para obter resultados conclusivos acerca da comparação das duas implementações C++ com uma implementação em C. O teste realizado concentra-se no módulo do driver UART. Toda a variabilidade nas interfaces do driver foram implementadas também com compilação condicional. Os resultados traduzem o tempo de execução, memória de código, e métricas de gestão de código, para uma aplicação sequencial que transmite e recebe um carácter e uma string via série. Resultados de Desempenho e Footprint de Memória Os resultados de desempenho traduzem os resultados a nível de tempo de execução. Estes indicam os ciclos de relógio necessários para executar o teste com cada uma das implementações - C++ template metaprogramming, C++ polimorfismo dinâmico, C compilação condicional. Os resultados de footprint de memória indicam qual 134

153 Capítulo 5. Resultados Experimentais a memória de código necessária para executar o teste com cada uma das implementações. Para obter os tempos de execução de cada uma das implementações do sistema operativo, foi utilizado o debugger do ambiente de desenvolvimento. Nessa avaliação não foi considerado o tempo que demora efetivamente a enviar ou receber o carácter via série. Por outro lado, para avaliar o tamanho da memória de código foi utilizado o ficheiro de código produzido para executar na plataforma de teste. (a) Tempo de execução (b) Memória de código Figura 5.5: Resultados de desempenho e footprint de memória (teste ao driver USART) Os gráficos da figura 5.5 apresentam os resultados do tempo de execução e memória de código das implementações C com compilação condicional (C CC), C++ com polimorfismo dinâmico (C++ PD), e C++ com template metaprogramming (C++ TMP), do driver, para a execução da aplicação anteriormente descrita. Tal como seria de esperar a implementação com TMP apresenta novamente um melhor desempenho e gestão da memória de código quando comparada com a implementação com polimorfismo dinâmico. Na comparação das implementações C (compilação condicional) e TMP, tal como os gráficos ilustram, a implementação C apresenta tanto um tempo de execução como dimensão de memória de código inferior a implementação TMP. No entanto, a diferença é relativamente mais baixa que a diferença existente entre as duas implementações com programação orientada a objetos. Por exemplo, a implementação TMP apenas agrava o desempenho em 5% e o footprint de memória em 20% quando 135

154 5.3. Testes Realizados comparada com a implementação C. Já a implementação com polimorfismo dinâmico agrava o desempenho em 17% e o footprint de memória em 75% quando comparada com a implementação em linguagem C. Os resultados apresentados traduzem apenas dois graus de variabilidade na funcionalidade em análise. Experiências realizadas pelo autor com mais graus de variabilidade indicam que os valores apresentados anteriormente na comparação entre a implementação TMP e a implementação C mantém-se praticamente constantes com o aumento da variabilidade. Contudo, quando se compara com a implementação com polimorfismo dinâmico, o agravamento nas métricas em análise pode ser muito superior (sobretudo em termos de footprint de memória) com o aumento da variabilidade na funcionalidade. Resultados de Gestão do Código Se as métricas de gestão e manutenção da variabilidade do código tenham sido importantes na interpretação dos resultados do teste realizado ao sistema operativo, então agora neste caso desempenham um papel preponderante. Isto porque como foi visto anteriormente, apesar da técnica de template metaprogramming ser muito mais otimizada que a implementação com polimorfismo dinâmico, esta agrava ligeiramente o desempenho e memória da aplicação quando comparada com a linguagem C. No entanto, como o overhead é relativamente baixo, as métricas de gestão de código desempenham um papel fundamental na comparação entre as mesmas. Os gráficos da figura 5.6 apresentam os valores das métricas LOC e NOC para as implementações C com compilação condicional (C CC), C++ com polimorfismo dinâmico (C++ PD), e C++ com template metaprogramming (C++ TMP), na funcionalidade em análise. Dos gráficos da figura 5.6, conclui-se que o número de linhas de linhas de código (LOC) das três implementações é praticamente o mesmo (ligeira superioridade para a implementação em C). No que diz respeito a métrica relacionado com o número de classes (NOC), a implementação C++ com template metaprogramming apresenta um valor superior ao da implementação C++ com polimorfismo dinâmico e C com compilação condicional. Aliás, a implementação em C, embora apresente uma ligeira melhoria no desempenho e footprint de memória que a implementação com TMP, não apresenta qualquer modularidade e encapsulamento no código. Em sistemas com enorme variabilidade, isso reflete-se numa degradação da organização do código, pois 136

155 Capítulo 5. Resultados Experimentais (a) Número de linhas de código (b) Número de classes Figura 5.6: Resultados de gestão do código (teste ao driver USART) este é poluído com as diretivas de pré-processador. Portanto, a gestão e manutenção deste tipo de sistemas torna-se uma tarefa fastidiosa e suscetível a erros, que acaba por não compensar os ganhos obtidos nas outras duas métricas. 137

156

157 Capítulo 6 Conclusões Neste último capitulo da dissertação, são apresentadas as ilações retiradas pelo autor, com base no que foi implementado. Além disso, são apresentadas algumas sugestões para melhorar e expandir o trabalho realizado. 6.1 Conclusão A dissertação apresenta o porting, expansão e customização de um sistema operativo orientado a objetos para a arquitetura MCS-51. No entanto, esta distingue-se essencialmente pela aplicação de template metaprogramming como metodologia para a gestão da variabilidade do sistema operativo. Este foi sem dúvida um projeto desafiante pela variedade e profundidade de conhecimentos necessários no domínio dos sistemas embebidos. Desde a compreensão de diferentes arquiteturas de processadores (80188 e 8051), passando pelos sistemas operativos (sobretudo de sistemas operativos de tempo-real baseados em microkernel), linguagem assembly, programação orientada a objetos (sobretudo C++), template metaprogramming e compiladores, todas estas temáticas foram utilizadas no desenvolvimento da dissertação. Relativamente aos objetivos do trabalho, estes foram efetivamente cumpridos. Depois de analisados alguns sistemas operativos orientados a objetos, o sistema operativo ADEOS foi selecionado como a melhor solução para os recursos da arquitetura alvo. Assim, foi realizado com sucesso o porting desse sistema operativo para a plataforma MCS-51. Depois disso, foram expandidas uma série de funcionalidades no sistema operativo, principalmente um conjunto de device drivers para comunicar 139

158 6.2. Trabalho Futuro com os periféricos do microcontrolador, bem como um escalonador power-aware para aplicações cujo principal foco seja o baixo consumo energético. O objetivo seguinte, e de todo o mais importante do trabalho, consistiu na aplicação de template metaprogramming para efetuar o refactoring do sistema operativo. Por outras palavras, a gestão da variabilidade do sistema foi realmente conseguida utilizando essa técnica de programação avançada. Finalmente, o último objetivo concretizado com sucesso focou-se na validação da premissa de que é possível utilizar C++ template metaprogramming (POO), sem comprometer consideravelmente o desempenho e recursos de memória, para implementar software embebido altamente customizável, reutilizável e de fácil gestão e manutenção. Os resultados obtidos demonstraram que isso é efetivamente possível à custa de um overhead reduzido. 6.2 Trabalho Futuro Apesar do cumprimento de todos os objetivos inicialmente propostos, existem bastantes funcionalidades e melhorias que podem expandir o trabalho desenvolvido. A primeira está relacionada com os device drivers. Conforme foi referido na secção 4.2.2, mais do que desenvolver controladores de hardware sob a forma de classes, o conceito de device drivers tem intrinsecamente associado uma determinada abstração, que implica disponibilizar serviços comuns a todos os dispositivos. Assim sendo, propõe-se o desenvolvimento de uma camada de abstração recorrendo a template metaprogramming para encapsular todos os periféricos na mesma interface. A segunda sugestão consiste na expansão das funcionalidades e da sua variabilidade. Mais do que implementar a própria variabilidade em cada funcionalidade, esta dissertação preocupou-se mais com a metodologia para gerir essa variabilidade. Assim sendo, na tentativa de expandir ainda mais o trabalho desenvolvido, propõe-se implementar mais mecanismos de IPC (semaphore, shared memory, message queue), mais algoritmos de escalonamento (rate-monotonic, round robin), mais device drivers (CAN, ADC, DAC), e mais variantes dos mesmos. A terceira sugestão diz respeito às interrupções. O sistema operativo não disponibiliza uma interface que permita configurar as interrupções do microcontrolador. Inclusive os device drivers foram implementados apenas com o mecanismo de polling. Assim sendo, propõe-se a expansão do sistema operativo com uma interface para configuração das interrupções disponibilizadas pelo

159 Capítulo 6. Conclusões A quarta sugestão está ligada aos resultados experimentais. Como foi possível constatar, a avaliação do sistema operativo nas métricas em causa só foi possível entre duas implementações: polimorfismo dinâmico e template metaprogramming. Isto porque implementar todo o sistema operativo e respetiva variabilidade em linguagem C tornava-se uma tarefa inexequível para o autor. Neste sentido, propõe-se a implementação do sistema operativo (e de todas as funcionalidades) em linguagem C, e consequente estudo comparativo das métricas de desempenho, footprint de memória e gestão do código. Desta forma, será possível sustentar fidedignamente os resultados aqui apresentados. A quinta e última sugestão propõe o porting do sistema operativo para outras plataformas. Basicamente, consiste na reimplementação do código dependente do processador para arquiteturas como a AVR ou ARM. Desta forma, reestruturando o IDE seria possível gerar o sistema operativo orientado a objetos customizado para diferentes arquiteturas alvo. Tudo de forma fácil e simplificada. 141

160

161 Apêndices

162

163 Apêndice A Placa Circuito Impresso: spi2c Para validar o código dos drivers SPI e I 2 C, o autor decidiu projetar e implementar um add-on para a plataforma de desenvolvimento de testes (8051DKUSB). Isto porque por si só, essa plataforma não dispõe de hardware capaz de comunicar com as interfaces desses protocolos do microcontrolador. O add-on designado spi2c foi concebido de forma a ser acoplado ao conector de expansão da placa 8051DKUSB. Desta forma é possível aceder facilmente aos pinos dedicados a cada um dos protocolos de comunicação. A nível de hardware, a placa vem equipada essencialmente com dois I/O expanders de 16-bit e dois conversores analógico-digital (ADC). Dos I/O expanders, ambos da Microchip Technology [79], o MCP23S17 [80] tem interface SPI, enquanto o MCP23017 [80] tem interface I 2 C. Quanto aos ADCs, o ADS7834 [81] tem interface SPI, e o ADS7823 [82] tem interface I 2 C. Nos pinos de ambos os I/O expanders são ligados LEDs, em lógica negada, para visualizar as saídas, bem como switchs para avaliar as entradas. Nas entradas dos ADCs são ligados divisores de tensão com potênciometro, para variar o valor da tensão lida. A alimentação do add-on é feita com a alimentação da plataforma de desenvolvimento, disponível no conector de acoplamento. São usadas resistências de polarização para limitar a corrente nos LEDs, resistências de pull-up nas linhas I 2 C, bem como alguns condensadores de desacoplamento. O esquemático e o layout da placa spi2c pode ser visto nas figuras A.1 e A.2, respetivamente. 145

164 146 Figura A.1: PCB spi2c: esquemático

165 Apêndice A. Placa Circuito Impresso: spi2c Figura A.2: PCB spi2c: layout 147

Prof. Marcos Ribeiro Quinet de Andrade Universidade Federal Fluminense - UFF Pólo Universitário de Rio das Ostras - PURO

Prof. Marcos Ribeiro Quinet de Andrade Universidade Federal Fluminense - UFF Pólo Universitário de Rio das Ostras - PURO Conceitos básicos e serviços do Sistema Operacional Prof. Marcos Ribeiro Quinet de Andrade Universidade Federal Fluminense - UFF Pólo Universitário de Rio das Ostras - PURO Tipos de serviço do S.O. O S.O.

Leia mais

Figura 01 Kernel de um Sistema Operacional

Figura 01 Kernel de um Sistema Operacional 01 INTRODUÇÃO 1.5 ESTRUTURA DOS SISTEMAS OPERACIONAIS O Sistema Operacional é formado por um Conjunto de rotinas (denominado de núcleo do sistema ou kernel) que oferece serviços aos usuários e suas aplicações

Leia mais

PROGRAMAÇÃO AVANÇADA -CONCEITOS DE ORIENTAÇÃO A OBJETOS. Prof. Angelo Augusto Frozza, M.Sc. frozza@ifc-camboriu.edu.br

PROGRAMAÇÃO AVANÇADA -CONCEITOS DE ORIENTAÇÃO A OBJETOS. Prof. Angelo Augusto Frozza, M.Sc. frozza@ifc-camboriu.edu.br PROGRAMAÇÃO AVANÇADA -CONCEITOS DE ORIENTAÇÃO A OBJETOS Prof. Angelo Augusto Frozza, M.Sc. frozza@ifc-camboriu.edu.br ROTEIRO 1. Conceitos de Orientação a Objetos Introdução O paradigma da POO Classes

Leia mais

4 Estrutura do Sistema Operacional. 4.1 - Kernel

4 Estrutura do Sistema Operacional. 4.1 - Kernel 1 4 Estrutura do Sistema Operacional 4.1 - Kernel O kernel é o núcleo do sistema operacional, sendo responsável direto por controlar tudo ao seu redor. Desde os dispositivos usuais, como unidades de disco,

Leia mais

3 Um Framework Orientado a Aspectos para Monitoramento e Análise de Processos de Negócio

3 Um Framework Orientado a Aspectos para Monitoramento e Análise de Processos de Negócio 32 3 Um Framework Orientado a Aspectos para Monitoramento e Análise de Processos de Negócio Este capítulo apresenta o framework orientado a aspectos para monitoramento e análise de processos de negócio

Leia mais

Hardware (Nível 0) Organização. Interface de Máquina (IM) Interface Interna de Microprogramação (IIMP)

Hardware (Nível 0) Organização. Interface de Máquina (IM) Interface Interna de Microprogramação (IIMP) Hardware (Nível 0) Organização O AS/400 isola os usuários das características do hardware através de uma arquitetura de camadas. Vários modelos da família AS/400 de computadores de médio porte estão disponíveis,

Leia mais

Sistemas Operacionais. Prof. M.Sc. Sérgio Teixeira. Aula 05 Estrutura e arquitetura do SO Parte 2. Cursos de Computação

Sistemas Operacionais. Prof. M.Sc. Sérgio Teixeira. Aula 05 Estrutura e arquitetura do SO Parte 2. Cursos de Computação Cursos de Computação Sistemas Operacionais Prof. M.Sc. Sérgio Teixeira Aula 05 Estrutura e arquitetura do SO Parte 2 Referência: MACHADO, F.B. ; MAIA, L.P. Arquitetura de Sistemas Operacionais. 4.ed. LTC,

Leia mais

Programação Estruturada e Orientada a Objetos. Fundamentos Orientação a Objetos

Programação Estruturada e Orientada a Objetos. Fundamentos Orientação a Objetos Programação Estruturada e Orientada a Objetos Fundamentos Orientação a Objetos 2013 O que veremos hoje? Introdução aos fundamentos de Orientação a Objetos Transparências baseadas no material do Prof. Jailton

Leia mais

Orientação a Objetos

Orientação a Objetos 1. Domínio e Aplicação Orientação a Objetos Um domínio é composto pelas entidades, informações e processos relacionados a um determinado contexto. Uma aplicação pode ser desenvolvida para automatizar ou

Leia mais

Sistemas Distribuídos

Sistemas Distribuídos Sistemas Distribuídos Modelo Cliente-Servidor: Introdução aos tipos de servidores e clientes Prof. MSc. Hugo Souza Iniciando o módulo 03 da primeira unidade, iremos abordar sobre o Modelo Cliente-Servidor

Leia mais

Sistemas Operacionais. Conceitos de um Sistema Operacional

Sistemas Operacionais. Conceitos de um Sistema Operacional Sistemas Operacionais Conceitos de um Sistema Operacional Modo usuário e Modo Kernel Como já vimos são ambientes de execução diferentes no processador Há um conjunto de funções privilegiadas acessadas

Leia mais

SISTEMAS OPERACIONAIS

SISTEMAS OPERACIONAIS SISTEMAS OPERACIONAIS Tópico 4 Estrutura do Sistema Operacional Prof. Rafael Gross prof.rafaelgross@fatec.sp.gov.br FUNÇÕES DO NUCLEO As principais funções do núcleo encontradas na maioria dos sistemas

Leia mais

Arquitetura de Sistemas Operacionais Machado/Maia. Arquitetura de Sistemas

Arquitetura de Sistemas Operacionais Machado/Maia. Arquitetura de Sistemas Arquitetura de Sistemas Operacionais Capítulo 4 Estrutura do Sistema Operacional Cap. 4 Estrutura do Sistema 1 Sistemas Operacionais Pitágoras Fadom Divinópolis Material Utilizado na disciplina Sistemas

Leia mais

IFPE. Disciplina: Sistemas Operacionais. Prof. Anderson Luiz Moreira

IFPE. Disciplina: Sistemas Operacionais. Prof. Anderson Luiz Moreira IFPE Disciplina: Sistemas Operacionais Prof. Anderson Luiz Moreira SERVIÇOS OFERECIDOS PELOS SOS 1 Introdução O SO é formado por um conjunto de rotinas (procedimentos) que oferecem serviços aos usuários

Leia mais

2 Diagrama de Caso de Uso

2 Diagrama de Caso de Uso Unified Modeling Language (UML) Universidade Federal do Maranhão UFMA Pós Graduação de Engenharia de Eletricidade Grupo de Computação Assunto: Diagrama de Caso de Uso (Use Case) Autoria:Aristófanes Corrêa

Leia mais

Sistemas Operacionais

Sistemas Operacionais Sistemas Operacionais Aula 6 Estrutura de Sistemas Operacionais Prof.: Edilberto M. Silva http://www.edilms.eti.br Baseado no material disponibilizado por: SO - Prof. Edilberto Silva Prof. José Juan Espantoso

Leia mais

Sistemas Operacionais 2014 Introdução. Alexandre Augusto Giron alexandre.a.giron@gmail.com

Sistemas Operacionais 2014 Introdução. Alexandre Augusto Giron alexandre.a.giron@gmail.com Sistemas Operacionais 2014 Introdução Alexandre Augusto Giron alexandre.a.giron@gmail.com Roteiro Sistemas Operacionais Histórico Estrutura de SO Principais Funções do SO Interrupções Chamadas de Sistema

Leia mais

LP II Estrutura de Dados. Introdução e Linguagem C. Prof. José Honorato F. Nunes honorato.nunes@ifbaiano.bonfim.edu.br

LP II Estrutura de Dados. Introdução e Linguagem C. Prof. José Honorato F. Nunes honorato.nunes@ifbaiano.bonfim.edu.br LP II Estrutura de Dados Introdução e Linguagem C Prof. José Honorato F. Nunes honorato.nunes@ifbaiano.bonfim.edu.br Resumo da aula Considerações Gerais Introdução a Linguagem C Variáveis e C Tipos de

Leia mais

Componentes de um Sistema de Operação

Componentes de um Sistema de Operação Componentes de um Sistema de Operação Em sistemas modernos é habitual ter-se as seguintes componentes ou módulos: Gestor de processos Gestor da memória principal Gestor da memória secundária Gestor do

Leia mais

Engenharia de Software III

Engenharia de Software III Engenharia de Software III Casos de uso http://dl.dropbox.com/u/3025380/es3/aula6.pdf (flavio.ceci@unisul.br) 09/09/2010 O que são casos de uso? Um caso de uso procura documentar as ações necessárias,

Leia mais

SISTEMAS OPERACIONAIS

SISTEMAS OPERACIONAIS SISTEMAS OPERACIONAIS Arquitetura Sistemas Operacionais Andreza Leite andreza.leite@univasf.edu.br Plano de Aula Sistemas monolíticos Sistemas em camadas Sistemas micro-núcleo Modelo Cliente-Servidor Máquinas

Leia mais

Desenvolvendo uma Arquitetura de Componentes Orientada a Serviço SCA

Desenvolvendo uma Arquitetura de Componentes Orientada a Serviço SCA Desenvolvendo uma Arquitetura de Componentes Orientada a Serviço SCA RESUMO Ricardo Della Libera Marzochi A introdução ao Service Component Architecture (SCA) diz respeito ao estudo dos principais fundamentos

Leia mais

Processos e Threads (partes I e II)

Processos e Threads (partes I e II) Processos e Threads (partes I e II) 1) O que é um processo? É qualquer aplicação executada no processador. Exe: Bloco de notas, ler um dado de um disco, mostrar um texto na tela. Um processo é um programa

Leia mais

Figura 1 - O computador

Figura 1 - O computador Organização e arquitectura dum computador Índice Índice... 2 1. Introdução... 3 2. Representação da informação no computador... 4 3. Funcionamento básico dum computador... 5 4. Estrutura do processador...

Leia mais

Sistemas Operacionais

Sistemas Operacionais UNIVERSIDADE BANDEIRANTE DE SÃO PAULO INSTITUTO POLITÉCNICO CURSO DE SISTEMAS DE INFORMAÇÃO Sistemas Operacionais Notas de Aulas: Tópicos 7 e 8 Estrutura do Sistema Operacional São Paulo 2009 1 Sumário

Leia mais

Aspectos técnicos do desenvolvimento baseado em componentes

Aspectos técnicos do desenvolvimento baseado em componentes Aspectos técnicos do desenvolvimento baseado em componentes Um novo processo de desenvolvimento O uso de componentes traz mudanças no processo de desenvolvimento Além de desenvolver um produto, queremos

Leia mais

UNIVERSIDADE FEDERAL DO PARANÁ UFPR Bacharelado em Ciência da Computação

UNIVERSIDADE FEDERAL DO PARANÁ UFPR Bacharelado em Ciência da Computação SOFT DISCIPLINA: Engenharia de Software AULA NÚMERO: 10 DATA: / / PROFESSOR: Andrey APRESENTAÇÃO O objetivo desta aula é apresentar e discutir os conceitos de coesão e acoplamento. DESENVOLVIMENTO Projetar

Leia mais

Sistemas Operacionais. Prof. M.Sc. Sérgio Teixeira. Aula 05 Estrutura e arquitetura do SO Parte 1. Cursos de Computação

Sistemas Operacionais. Prof. M.Sc. Sérgio Teixeira. Aula 05 Estrutura e arquitetura do SO Parte 1. Cursos de Computação Cursos de Computação Sistemas Operacionais Prof. M.Sc. Sérgio Teixeira Aula 05 Estrutura e arquitetura do SO Parte 1 Referência: MACHADO, F.B. ; MAIA, L.P. Arquitetura de Sistemas Operacionais. 4.ed. LTC,

Leia mais

Programação Concorrente em java - Exercícios Práticos Abril 2004

Programação Concorrente em java - Exercícios Práticos Abril 2004 Programação Concorrente em java - Exercícios Práticos Abril 2004 1. Introdução As threads correspondem a linhas de controlo independentes no âmbito de um mesmo processo. No caso da linguagem JAVA, é precisamente

Leia mais

Entrada e Saída. Interface entre periféricos, processador e memória. Fonte: Minho - Portugal 1

Entrada e Saída. Interface entre periféricos, processador e memória. Fonte: Minho - Portugal 1 Entrada e Saída Interface entre periféricos, processador e memória Fonte: Minho - Portugal 1 Ligação Processador/Memória - Periférico Processador Memória Controlo Dados Controlador Fonte: Minho - Portugal

Leia mais

Programação Funcional. Capítulo 1. Introdução. José Romildo Malaquias. Departamento de Computação Universidade Federal de Ouro Preto 2015.

Programação Funcional. Capítulo 1. Introdução. José Romildo Malaquias. Departamento de Computação Universidade Federal de Ouro Preto 2015. Programação Funcional Capítulo 1 Introdução José Romildo Malaquias Departamento de Computação Universidade Federal de Ouro Preto 2015.1 1/13 1 Paradigmas de programação 2 Programação funcional 3 A Crise

Leia mais

Sistemas Operacionais

Sistemas Operacionais Sistemas Operacionais Aula 07 Arquitetura de Sistemas Operacionais Prof. Maxwell Anderson www.maxwellanderson.com.br Introdução Conceitos já vistos em aulas anteriores: Definição de Sistemas Operacionais

Leia mais

Curso de Java. Orientação a objetos e a Linguagem JAVA. TodososdireitosreservadosKlais

Curso de Java. Orientação a objetos e a Linguagem JAVA. TodososdireitosreservadosKlais Curso de Java Orientação a objetos e a Linguagem JAVA Roteiro A linguagem Java e a máquina virtual Objetos e Classes Encapsulamento, Herança e Polimorfismo Primeiro Exemplo A Linguagem JAVA Principais

Leia mais

O que veremos nesta aula? Principais Aspectos de Sistemas Operacionais. Visão geral de um sistema computacional

O que veremos nesta aula? Principais Aspectos de Sistemas Operacionais. Visão geral de um sistema computacional O que veremos nesta aula? Principais Aspectos de Sistemas Operacionais Laboratório de Sistemas Operacionais Aula 1 Flávia Maristela (flavia@flaviamaristela.com) Tudo o que já vimos antes... Introdução

Leia mais

Programação de Sistemas

Programação de Sistemas Programação de Sistemas Introdução à gestão de memória Programação de Sistemas Gestão de memória : 1/16 Introdução (1) A memória central de um computador é escassa. [1981] IBM PC lançado com 64KB na motherboard,

Leia mais

Engenharia de software para desenvolvimento com LabVIEW: Validação

Engenharia de software para desenvolvimento com LabVIEW: Validação Engenharia de software para desenvolvimento com LabVIEW: Orientação a Objetos, Statechart e Validação André Pereira Engenheiro de Vendas (Grande São Paulo) Alexsander Loula Coordenador Suporte Técnico

Leia mais

3/9/2010. Ligação da UCP com o barramento do. sistema. As funções básicas dos registradores nos permitem classificá-los em duas categorias:

3/9/2010. Ligação da UCP com o barramento do. sistema. As funções básicas dos registradores nos permitem classificá-los em duas categorias: Arquitetura de Computadores Estrutura e Funcionamento da CPU Prof. Marcos Quinet Universidade Federal Fluminense P.U.R.O. Revisão dos conceitos básicos O processador é o componente vital do sistema de

Leia mais

Capítulo 8. Software de Sistema

Capítulo 8. Software de Sistema Capítulo 8 Software de Sistema Adaptado dos transparentes das autoras do livro The Essentials of Computer Organization and Architecture Objectivos Conhecer o ciclo de desenvolvimento da linguagem Java

Leia mais

ESTUDO DE CASO WINDOWS VISTA

ESTUDO DE CASO WINDOWS VISTA ESTUDO DE CASO WINDOWS VISTA História Os sistemas operacionais da Microsoft para PCs desktop e portáteis e para servidores podem ser divididos em 3 famílias: MS-DOS Windows baseado em MS-DOS Windows baseado

Leia mais

SISTEMAS OPERACIONAIS. Apostila 03 Estrutura do Sistema Operacional UNIBAN

SISTEMAS OPERACIONAIS. Apostila 03 Estrutura do Sistema Operacional UNIBAN SISTEMAS OPERACIONAIS Apostila 03 Estrutura do Sistema Operacional UNIBAN 1.0 O Sistema Operacional como uma Máquina Virtual A arquitetura (conjunto de instruções, organização de memória, E/S e estrutura

Leia mais

Especificação do 3º Trabalho

Especificação do 3º Trabalho Especificação do 3º Trabalho I. Introdução O objetivo deste trabalho é abordar a prática da programação orientada a objetos usando a linguagem Java envolvendo os conceitos de classe, objeto, associação,

Leia mais

Sistemas Operacionais

Sistemas Operacionais Sistemas Operacionais Gerência de processos Controle e descrição de processos Edson Moreno edson.moreno@pucrs.br http://www.inf.pucrs.br/~emoreno Sumário Representação e controle de processos pelo SO Estrutura

Leia mais

Sistemas Operacionais Gerência de Dispositivos

Sistemas Operacionais Gerência de Dispositivos Universidade Estadual de Mato Grosso do Sul UEMS Curso de Licenciatura em Computação Sistemas Operacionais Gerência de Dispositivos Prof. José Gonçalves Dias Neto profneto_ti@hotmail.com Introdução A gerência

Leia mais

Java. Marcio de Carvalho Victorino www.dominandoti.eng.br

Java. Marcio de Carvalho Victorino www.dominandoti.eng.br Java Marcio de Carvalho Victorino www.dominandoti.eng.br 3. Considere as instruções Java abaixo: int cont1 = 3; int cont2 = 2; int cont3 = 1; cont1 += cont3++; cont1 -= --cont2; cont3 = cont2++; Após a

Leia mais

Grupo I [6v] Considere o seguinte extracto de um programa de definição de uma calculadora apenas com a função soma de dois valores reais

Grupo I [6v] Considere o seguinte extracto de um programa de definição de uma calculadora apenas com a função soma de dois valores reais Número: Nome: Página 1 de 5 LEIC/LERC 2012/13, Repescagem do 1º Teste de Sistemas Distribuídos, 25 de Junho de 2013 Responda no enunciado, apenas no espaço fornecido. Identifique todas as folhas. Duração:

Leia mais

Arquitetura de Computadores. Introdução aos Sistemas Operacionais

Arquitetura de Computadores. Introdução aos Sistemas Operacionais Arquitetura de Computadores Introdução aos Sistemas Operacionais O que é um Sistema Operacional? Programa que atua como um intermediário entre um usuário do computador ou um programa e o hardware. Os 4

Leia mais

Introdução à Computação

Introdução à Computação Aspectos Importantes - Desenvolvimento de Software Motivação A economia de todos países dependem do uso de software. Cada vez mais, o controle dos processos tem sido feito por software. Atualmente, os

Leia mais

Ao longo do presente capítulo será apresentada uma descrição introdutória da tecnologia FPGA e dos módulos básicos que a constitui.

Ao longo do presente capítulo será apresentada uma descrição introdutória da tecnologia FPGA e dos módulos básicos que a constitui. 3 Tecnologia FPGA Ao longo do presente capítulo será apresentada uma descrição introdutória da tecnologia FPGA e dos módulos básicos que a constitui. 3.1. FPGA: Histórico, linguagens e blocos Muitos dos

Leia mais

6 - Gerência de Dispositivos

6 - Gerência de Dispositivos 1 6 - Gerência de Dispositivos 6.1 Introdução A gerência de dispositivos de entrada/saída é uma das principais e mais complexas funções do sistema operacional. Sua implementação é estruturada através de

Leia mais

Sistemas Operacionais

Sistemas Operacionais Sistemas Operacionais Aula 11 Sincronização de Processos Prof.: Edilberto M. Silva http://www.edilms.eti.br Baseado no material disponibilizado por: SO - Prof. Edilberto Silva Prof. José Juan Espantoso

Leia mais

Tabela de Símbolos. Análise Semântica A Tabela de Símbolos. Principais Operações. Estrutura da Tabela de Símbolos. Declarações 11/6/2008

Tabela de Símbolos. Análise Semântica A Tabela de Símbolos. Principais Operações. Estrutura da Tabela de Símbolos. Declarações 11/6/2008 Tabela de Símbolos Análise Semântica A Tabela de Símbolos Fabiano Baldo Após a árvore de derivação, a tabela de símbolos é o principal atributo herdado em um compilador. É possível, mas não necessário,

Leia mais

1. CAPÍTULO COMPUTADORES

1. CAPÍTULO COMPUTADORES 1. CAPÍTULO COMPUTADORES 1.1. Computadores Denomina-se computador uma máquina capaz de executar variados tipos de tratamento automático de informações ou processamento de dados. Os primeiros eram capazes

Leia mais

UNIVERSIDADE FEDERAL DE SANTA MARIA CENTRO DE TECNOLOGIA AULA 14 PROFª BRUNO CALEGARO

UNIVERSIDADE FEDERAL DE SANTA MARIA CENTRO DE TECNOLOGIA AULA 14 PROFª BRUNO CALEGARO UNIVERSIDADE FEDERAL DE SANTA MARIA CENTRO DE TECNOLOGIA AULA 14 PROFª BRUNO CALEGARO Santa Maria, 01 de Novembro de 2013. Revisão aula passada Projeto de Arquitetura Decisões de projeto de Arquitetura

Leia mais

UML Aspectos de projetos em Diagramas de classes

UML Aspectos de projetos em Diagramas de classes UML Aspectos de projetos em Diagramas de classes Após ser definido o contexto da aplicação a ser gerada. Devemos pensar em detalhar o Diagrama de Classes com informações visando uma implementação Orientada

Leia mais

SISTEMAS OPERACIONAIS. Prof. André Dutton

SISTEMAS OPERACIONAIS. Prof. André Dutton 1 SISTEMAS OPERACIONAIS Prof. André Dutton O OS esta ligado diretamente com o Hardware do Computador no qual ele é executado. CPU MEMORIA CONTROLAD OR DE VIDEO CONTROLAD OR DE TECLADO CONTROLAD OR DE DISCO

Leia mais

Integração de Sistemas Embebidos MECom :: 5º ano

Integração de Sistemas Embebidos MECom :: 5º ano Integração de Sistemas Embebidos MECom :: 5º ano Device Drivers em Linux - Introdução António Joaquim Esteves www.di.uminho.pt/~aje Bibliografia: capítulo 1, LDD 3ed, O Reilly DEP. DE INFORMÁTICA ESCOLA

Leia mais

Organização e Arquitetura de Computadores I. de Computadores

Organização e Arquitetura de Computadores I. de Computadores Universidade Federal de Campina Grande Unidade Acadêmica de Sistemas e Computação Curso de Bacharelado em Ciência da Computação Organização e Arquitetura de Computadores I Organização Básica B de Computadores

Leia mais

Sistemas Operacionais Processos e Threads

Sistemas Operacionais Processos e Threads Sistemas Operacionais Processos e Threads Prof. Marcos Monteiro, MBA http://www.marcosmonteiro.com.br contato@marcosmonteiro.com.br 1 Estrutura de um Sistema Operacional 2 GERÊNCIA DE PROCESSOS Um processo

Leia mais

Sistemas Distribuídos. Professora: Ana Paula Couto DCC 064

Sistemas Distribuídos. Professora: Ana Paula Couto DCC 064 Sistemas Distribuídos Professora: Ana Paula Couto DCC 064 Processos- Clientes, Servidores, Migração Capítulo 3 Agenda Clientes Interfaces de usuário em rede Sistema X Window Software do lado cliente para

Leia mais

EXERCÍCIOS SOBRE ORIENTAÇÃO A OBJETOS

EXERCÍCIOS SOBRE ORIENTAÇÃO A OBJETOS Campus Cachoeiro de Itapemirim Curso Técnico em Informática Disciplina: Análise e Projeto de Sistemas Professor: Rafael Vargas Mesquita Este exercício deve ser manuscrito e entregue na próxima aula; Valor

Leia mais

Introdução aos Sistemas Operativos

Introdução aos Sistemas Operativos Introdução aos Sistemas Operativos Computadores e Redes de Comunicação Mestrado em Gestão de Informação, FEUP 06/07 Sérgio Sobral Nunes mail: sergio.nunes@fe.up.pt web: www.fe.up.pt/~ssn Sumário Definição

Leia mais

Sistemas Operacionais. Prof. André Y. Kusumoto andrekusumoto.unip@gmail.com

Sistemas Operacionais. Prof. André Y. Kusumoto andrekusumoto.unip@gmail.com Sistemas Operacionais Prof. André Y. Kusumoto andrekusumoto.unip@gmail.com Estruturas de Sistemas Operacionais Um sistema operacional fornece o ambiente no qual os programas são executados. Internamente,

Leia mais

Visão Geral de Sistemas Operacionais

Visão Geral de Sistemas Operacionais Visão Geral de Sistemas Operacionais Sumário Um sistema operacional é um intermediário entre usuários e o hardware do computador. Desta forma, o usuário pode executar programas de forma conveniente e eficiente.

Leia mais

3. O NIVEL DA LINGUAGEM DE MONTAGEM

3. O NIVEL DA LINGUAGEM DE MONTAGEM 3. O NIVEL DA LINGUAGEM DE MONTAGEM Nas aulas anteriores tivemos a oportunidade de discutir dois diferentes níveis presentes na maioria dos computadores atuais. Nesta aula dedica-se a outro nível que também

Leia mais

11/3/2009. Software. Sistemas de Informação. Software. Software. A Construção de um programa de computador. A Construção de um programa de computador

11/3/2009. Software. Sistemas de Informação. Software. Software. A Construção de um programa de computador. A Construção de um programa de computador Sistemas de Informação Prof. Anderson D. Moura Um programa de computador é composto por uma seqüência de instruções, que é interpretada e executada por um processador ou por uma máquina virtual. Em um

Leia mais

Introdução aos Computadores

Introdução aos Computadores Os Computadores revolucionaram as formas de processamento de Informação pela sua capacidade de tratar grandes quantidades de dados em curto espaço de tempo. Nos anos 60-80 os computadores eram máquinas

Leia mais

GereComSaber. Disciplina de Desenvolvimento de Sistemas de Software. Sistema de Gestão de Serviços em Condomínios

GereComSaber. Disciplina de Desenvolvimento de Sistemas de Software. Sistema de Gestão de Serviços em Condomínios Universidade do Minho Conselho de Cursos de Engenharia Licenciatura em Engenharia Informática 3ºAno Disciplina de Desenvolvimento de Sistemas de Software Ano Lectivo de 2009/2010 GereComSaber Sistema de

Leia mais

Esta dissertação apresentou duas abordagens para integração entre a linguagem Lua e o Common Language Runtime. O objetivo principal da integração foi

Esta dissertação apresentou duas abordagens para integração entre a linguagem Lua e o Common Language Runtime. O objetivo principal da integração foi 5 Conclusão Esta dissertação apresentou duas abordagens para integração entre a linguagem Lua e o Common Language Runtime. O objetivo principal da integração foi permitir que scripts Lua instanciem e usem

Leia mais

DEPARTAMENTO DE MATEMÁTICA E CIÊNCIAS EXPERIMENTAIS

DEPARTAMENTO DE MATEMÁTICA E CIÊNCIAS EXPERIMENTAIS DEPARTAMENTO DE MATEMÁTICA E CIÊNCIAS EXPERIMENTAIS Planificação Anual da Disciplina de TIC Módulos 1,2,3-10.ºD CURSO PROFISSIONAL DE TÉCNICO DE APOIO À GESTÃO DESPORTIVA Ano Letivo 2015-2016 Manual adotado:

Leia mais

UML - Unified Modeling Language

UML - Unified Modeling Language UML - Unified Modeling Language Casos de Uso Marcio E. F. Maia Disciplina: Engenharia de Software Professora: Rossana M. C. Andrade Curso: Ciências da Computação Universidade Federal do Ceará 24 de abril

Leia mais

Capacidade = 512 x 300 x 20000 x 2 x 5 = 30.720.000.000 30,72 GB

Capacidade = 512 x 300 x 20000 x 2 x 5 = 30.720.000.000 30,72 GB Calculando a capacidade de disco: Capacidade = (# bytes/setor) x (méd. # setores/trilha) x (# trilhas/superfície) x (# superfícies/prato) x (# pratos/disco) Exemplo 01: 512 bytes/setor 300 setores/trilha

Leia mais

Notas da Aula 15 - Fundamentos de Sistemas Operacionais

Notas da Aula 15 - Fundamentos de Sistemas Operacionais Notas da Aula 15 - Fundamentos de Sistemas Operacionais 1. Software de Entrada e Saída: Visão Geral Uma das tarefas do Sistema Operacional é simplificar o acesso aos dispositivos de hardware pelos processos

Leia mais

BARRAMENTO DO SISTEMA

BARRAMENTO DO SISTEMA BARRAMENTO DO SISTEMA Memória Principal Processador Barramento local Memória cachê/ ponte Barramento de sistema SCSI FireWire Dispositivo gráfico Controlador de vídeo Rede Local Barramento de alta velocidade

Leia mais

SISTEMAS OPERACIONAIS

SISTEMAS OPERACIONAIS SISTEMAS OPERACIONAIS Turma de Redes AULA 06 www.eduardosilvestri.com.br silvestri@eduardosilvestri.com.br Estrutura do Sistema Operacional Introdução É bastante complexo a estrutura de um sistema operacional,

Leia mais

SISTEMAS OPERACIONAIS 2007

SISTEMAS OPERACIONAIS 2007 SISTEMAS OPERACIONAIS 2007 VISÃO GERAL Sumário Conceito Máquina de Níveis Conceituação de SO Componentes do SO Visões do SO Conceito de Sistemas O que se espera de um sistema de computação? Execução de

Leia mais

SISTEMAS OPERACIONAIS CAPÍTULO 3 CONCORRÊNCIA

SISTEMAS OPERACIONAIS CAPÍTULO 3 CONCORRÊNCIA SISTEMAS OPERACIONAIS CAPÍTULO 3 CONCORRÊNCIA 1. INTRODUÇÃO O conceito de concorrência é o princípio básico para o projeto e a implementação dos sistemas operacionais multiprogramáveis. O sistemas multiprogramáveis

Leia mais

Componentes de um Sistema de Operação

Componentes de um Sistema de Operação Componentes de um Sistema de Operação Em sistemas modernos é habitual ter-se os seguintes componentes ou módulos: Gestor de processos Gestor da memória principal Gestor da memória secundária Gestor do

Leia mais

INTRODUÇÃO ÀS LINGUAGENS DE PROGRAMAÇÃO

INTRODUÇÃO ÀS LINGUAGENS DE PROGRAMAÇÃO Capítulo 1 INTRODUÇÃO ÀS LINGUAGENS DE PROGRAMAÇÃO 1.1 Histórico de Linguagens de Programação Para um computador executar uma dada tarefa é necessário que se informe a ele, de uma maneira clara, como ele

Leia mais

CAPÍTULO 7 NÍVEL DE LINGUAGEM DE MONTAGEM

CAPÍTULO 7 NÍVEL DE LINGUAGEM DE MONTAGEM CAPÍTULO 7 NÍVEL DE LINGUAGEM DE MONTAGEM 71 Introdução Difere dos níveis inferiores por ser implementado por tradução A tradução é usada quando um processador está disponível para uma mensagem fonte mas

Leia mais

Tabela de roteamento

Tabela de roteamento Existem duas atividades que são básicas a um roteador. São elas: A determinação das melhores rotas Determinar a melhor rota é definir por qual enlace uma determinada mensagem deve ser enviada para chegar

Leia mais

Capítulo 4. MARIE (Machine Architecture Really Intuitive and Easy)

Capítulo 4. MARIE (Machine Architecture Really Intuitive and Easy) Capítulo 4 João Lourenço Joao.Lourenco@di.fct.unl.pt Faculdade de Ciências e Tecnologia Universidade Nova de Lisboa 2007-2008 MARIE (Machine Architecture Really Intuitive and Easy) Adaptado dos transparentes

Leia mais

Introdução a Java. Hélder Nunes

Introdução a Java. Hélder Nunes Introdução a Java Hélder Nunes 2 Exercício de Fixação Os 4 elementos básicos da OO são os objetos, as classes, os atributos e os métodos. A orientação a objetos consiste em considerar os sistemas computacionais

Leia mais

Gestor de Processos Núcleo do Sistema Operativo. Sistemas Operativos 2011 / 2012. Gestor de Processos

Gestor de Processos Núcleo do Sistema Operativo. Sistemas Operativos 2011 / 2012. Gestor de Processos Gestor de Processos Núcleo do Sistema Operativo Sistemas Operativos 2011 / 2012 Gestor de Processos Entidade do núcleo responsável por suportar a execução dos processos Gestão das Interrupções Multiplexagem

Leia mais

Padrões de projeto 1

Padrões de projeto 1 Padrões de projeto 1 Design Orientado Objeto Encapsulamento Herança Polimorfismo Design Patterns 2 Responsabilidades Booch e Rumbaugh Responsabilidade é um contrato ou obrigação de um tipo ou classe. Dois

Leia mais

Feature-Driven Development

Feature-Driven Development FDD Feature-Driven Development Descrição dos Processos Requisitos Concepção e Planejamento Mais forma que conteúdo Desenvolver um Modelo Abrangente Construir a Lista de Features Planejar por

Leia mais

ENGENHARIA DE SOFTWARE

ENGENHARIA DE SOFTWARE ENGENHARIA DE SOFTWARE PARTE 2 LINGUAGEM DE MODELAÇÃO UML CAP. 8 UML MODELAÇÃO DA ARQUITETURA Tópicos Conceito de Diagramas Físicos Fundamentos dos Diagramas de Componentes componentes interface quando

Leia mais

ESTUDO COMPARATIVO ENTRE AS PLATAFORMAS ARDUINO E PIC

ESTUDO COMPARATIVO ENTRE AS PLATAFORMAS ARDUINO E PIC ESTUDO COMPARATIVO ENTRE AS PLATAFORMAS ARDUINO E PIC Tiago Menezes Xavier de Souza¹, Igor dos Passos Granado¹, Wyllian Fressatti¹ ¹Universidade Paranaense (UNIPAR) Paranavaí- PR- Brasil tiago_x666@hotmail.com,

Leia mais

Arquitecturas de Software Licenciatura em Engenharia Informática e de Computadores

Arquitecturas de Software Licenciatura em Engenharia Informática e de Computadores UNIVERSIDADE TÉCNICA DE LISBOA INSTITUTO SUPERIOR TÉCNICO Arquitecturas de Software Licenciatura em Engenharia Informática e de Computadores Primeiro Teste 21 de Outubro de 2006, 9:00H 10:30H Nome: Número:

Leia mais

LINGUAGENS E PARADIGMAS DE PROGRAMAÇÃO. Ciência da Computação IFSC Lages. Prof. Wilson Castello Branco Neto

LINGUAGENS E PARADIGMAS DE PROGRAMAÇÃO. Ciência da Computação IFSC Lages. Prof. Wilson Castello Branco Neto LINGUAGENS E PARADIGMAS DE PROGRAMAÇÃO Ciência da Computação IFSC Lages. Prof. Wilson Castello Branco Neto Conceitos de Linguagens de Roteiro: Apresentação do plano de ensino; Apresentação do plano de

Leia mais

Polimorfismo. Prof. Leonardo Barreto Campos 1

Polimorfismo. Prof. Leonardo Barreto Campos 1 Polimorfismo Prof. Leonardo Barreto Campos 1 Sumário Introdução; Polimorfismo; Polimorfismo Java; Métodos Abstratos Java Classes Abstratas Java Exercício - Java Polimorfismo C++ Classe Abstrata C++; Funções

Leia mais

Componentes de um Sistema de Operação

Componentes de um Sistema de Operação Componentes de um Sistema de Operação Em sistemas modernos é habitual ter-se os seguintes componentes ou módulos: Gestor de processos Gestor da memória principal Gestor da memória secundária Gestor do

Leia mais

Sistemas Distribuídos

Sistemas Distribuídos Sistemas Distribuídos Processos I: Threads, virtualização e comunicação via protocolos Prof. MSc. Hugo Souza Nesta primeira parte sobre os Processos Distribuídos iremos abordar: Processos e a comunicação

Leia mais

SISTEMAS OPERACIONAIS ABERTOS Prof. Ricardo Rodrigues Barcelar http://www.ricardobarcelar.com

SISTEMAS OPERACIONAIS ABERTOS Prof. Ricardo Rodrigues Barcelar http://www.ricardobarcelar.com - Aula 2-1. PRINCÍPIOS DE SOFTWARE DE ENTRADA E SAÍDA (E/S) As metas gerais do software de entrada e saída é organizar o software como uma série de camadas, com as mais baixas preocupadas em esconder as

Leia mais

TÉCNICAS DE PROGRAMAÇÃO

TÉCNICAS DE PROGRAMAÇÃO TÉCNICAS DE PROGRAMAÇÃO (Adaptado do texto do prof. Adair Santa Catarina) ALGORITMOS COM QUALIDADE MÁXIMAS DE PROGRAMAÇÃO 1) Algoritmos devem ser feitos para serem lidos por seres humanos: Tenha em mente

Leia mais

DEPARTAMENTO DE ENGENHARIA INFORMÁTICA FACULDADE DE CIÊNCIAS E TECNOLOGIA DA UNIVERSIDADE DE COIMBRA

DEPARTAMENTO DE ENGENHARIA INFORMÁTICA FACULDADE DE CIÊNCIAS E TECNOLOGIA DA UNIVERSIDADE DE COIMBRA DEPARTAMENTO DE ENGENHARIA INFORMÁTICA FACULDADE DE CIÊNCIAS E TECNOLOGIA DA UNIVERSIDADE DE COIMBRA Sistemas Operativos 2003/2004 Trabalho Prático #2 -- Programação em C com ponteiros -- Objectivos Familiarização

Leia mais

Sistemas Operacionais. Prof. André Y. Kusumoto andrekusumoto.unip@gmail.com

Sistemas Operacionais. Prof. André Y. Kusumoto andrekusumoto.unip@gmail.com Sistemas Operacionais Prof. André Y. Kusumoto andrekusumoto.unip@gmail.com Estruturas de Sistemas de Computação O sistema operacional precisa garantir a operação correta do sistema de computação. Operação

Leia mais

Análise e Projeto Orientados por Objetos

Análise e Projeto Orientados por Objetos Análise e Projeto Orientados por Objetos Aula 02 Análise e Projeto OO Edirlei Soares de Lima Análise A análise modela o problema e consiste das atividades necessárias para entender

Leia mais