Universidade do Minho
Escola de Engenharia
Sandro Emanuel Salgado Pinto
UMinho | 2012
Sandro Emanuel Salgado Pinto Sistema Operativo Orientado a Objetos:
porting, expansão e configuração
Sistema Operativo Orientado a Objetos:
porting, expansão e configuração
Outubro de 2012
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
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
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
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
Conteúdo
1 Introdução
1
1.1
Contextualização . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
1.2
Objetivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3
1.3
Organização da Dissertação . . . . . . . . . . . . . . . . . . . . . . .
4
2 Estado da Arte
2.1
2.2
2.3
2.4
Programação Orientada a Objetos . . . . . . . . . . . . . . . . . . . .
5
2.1.1
Paradigmas de Programação . . . . . . . . . . . . . . . . . . .
6
2.1.2
Objetos e Classes . . . . . . . . . . . . . . . . . . . . . . . . .
7
2.1.3
Princı́pios Fundamentais . . . . . . . . . . . . . . . . . . . . .
8
Sistemas Operativos . . . . . . . . . . . . . . . . . . . . . . . . . . .
12
2.2.1
Arquitetura dos Sistemas Operativos . . . . . . . . . . . . . .
12
2.2.2
Sistemas Operativos de Tempo-Real . . . . . . . . . . . . . . .
14
2.2.3
Sistemas Operativos Orientados a Objetos . . . . . . . . . . .
15
Configurabilidade e Variabilidade no Software: técnicas de programação 21
2.3.1
Compilação Condicional . . . . . . . . . . . . . . . . . . . . .
22
2.3.2
Orientação a Objetos . . . . . . . . . . . . . . . . . . . . . . .
23
2.3.3
Orientação a Componentes . . . . . . . . . . . . . . . . . . . .
23
2.3.4
Orientação a Funcionalidades . . . . . . . . . . . . . . . . . .
24
2.3.5
Orientação a Aspetos . . . . . . . . . . . . . . . . . . . . . . .
25
2.3.6
Programação Generativa . . . . . . . . . . . . . . . . . . . . .
26
Conclusões . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
28
3 Especificação do Sistema
3.1
5
29
Microcontrolador 8051 . . . . . . . . . . . . . . . . . . . . . . . . . .
29
3.1.1
31
Arquitetura de Memória . . . . . . . . . . . . . . . . . . . . .
ix
3.2
3.3
3.4
3.1.2
Registos Básicos . . . . . . . . . . . . . . . . . . . . . . . . . .
32
3.1.3
Periféricos . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33
3.1.4
Interrupções . . . . . . . . . . . . . . . . . . . . . . . . . . . .
34
3.1.5
Arquitetura do Conjunto de Instruções . . . . . . . . . . . . .
35
ADEOS: A Decent Embedded Operating System . . . . . . . . . . . .
37
3.2.1
Tarefas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
38
3.2.2
Escalonador . . . . . . . . . . . . . . . . . . . . . . . . . . . .
42
3.2.3
Sincronização de Tarefas . . . . . . . . . . . . . . . . . . . . .
45
Template MetaProgramming . . . . . . . . . . . . . . . . . . . . . . .
47
3.3.1
Blocos Básicos do Template Metaprogramming . . . . . . . . .
47
3.3.2
O Fatorial . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
49
3.3.3
Lista Ligada Estática . . . . . . . . . . . . . . . . . . . . . . .
51
Ambiente de Desenvolvimento . . . . . . . . . . . . . . . . . . . . . .
52
3.4.1
55
Compilador IAR C/C++ para o 8051 . . . . . . . . . . . . . .
4 Implementação do Sistema
4.1
4.2
4.3
Porting do ADEOS para a Plataforma MCS-51 . . . . . . . . . . . .
65
4.1.1
Análise do Código Dependente do Processador . . . . . . . . .
67
4.1.2
Porting do Código Dependente do Processador . . . . . . . . .
75
Upgrade do ADEOS . . . . . . . . . . . . . . . . . . . . . . . . . . .
82
4.2.1
Upgrade: clock-tick no escalonador . . . . . . . . . . . . . . .
83
4.2.2
Upgrade: device drivers . . . . . . . . . . . . . . . . . . . . . .
85
4.2.3
Upgrade: escalonador power-aware . . . . . . . . . . . . . . . 108
Refactoring do ADEOS . . . . . . . . . . . . . . . . . . . . . . . . . . 114
4.3.1
Diagrama de Funcionalidades . . . . . . . . . . . . . . . . . . 114
4.3.2
Estratégia de Gestão da Variabilidade . . . . . . . . . . . . . . 117
4.3.3
Reestruturação do ADEOS . . . . . . . . . . . . . . . . . . . . 120
5 Resultados Experimentais
x
65
127
5.1
Ambiente de Testes . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
5.2
Métricas de Teste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
5.3
Testes Realizados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
5.3.1
Teste ao Sistema Operativo . . . . . . . . . . . . . . . . . . . 131
5.3.2
Teste ao driver USART . . . . . . . . . . . . . . . . . . . . . 134
6 Conclusões
139
6.1 Conclusão . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
6.2 Trabalho Futuro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
A Placa Circuito Impresso: spi2c
145
xi
Lista de Figuras
2.1
Modelos arquiteturais de um sistema operativo: monolı́tico e microkernel 13
2.2
Diagrama de classes do core do sistema operativo BOSS . . . . . . .
20
2.3
Orientação a funcionalidades: hierarquia de classes e modelo de funcionalidades . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
25
2.4
Junção do código aspeto no código dos componentes . . . . . . . . . .
27
3.1
Diagrama de blocos do microcontrolador 8051 clássico . . . . . . . . .
30
3.2
Mapa de memória do 8051 genérico . . . . . . . . . . . . . . . . . . .
32
3.3
Diagrama de classes do ADEOS . . . . . . . . . . . . . . . . . . . . .
38
3.4
Relação dos estados das tarefas no ADEOS . . . . . . . . . . . . . . .
40
3.5
Ilustração da lista de tarefas prontas a executar (readyList) . . . . . .
44
3.6
Resolução dos templates no cálculo do fatorial . . . . . . . . . . . . .
51
3.7
Processo de compilação de código fonte em código executável/máquina 54
4.1
Arquitetura de software do ADEOS . . . . . . . . . . . . . . . . . . .
66
4.2
Pilha do sistema após entrada na função contextInit . . . . . . . . . .
69
4.3
Pilha da tarefa após inicialização . . . . . . . . . . . . . . . . . . . .
71
4.4
Diagrama de classes do driver PWM . . . . . . . . . . . . . . . . . .
87
4.5
Diagrama de classes do driver UART . . . . . . . . . . . . . . . . . .
92
4.6
Diagrama de classes do driver GPIO . . . . . . . . . . . . . . . . . .
96
4.7
Formato da trama I 2 C . . . . . . . . . . . . . . . . . . . . . . . . . .
99
2
4.8
Diagrama de classes do driver I C . . . . . . . . . . . . . . . . . . . 100
4.9
SPI: barramento e diagrama temporal . . . . . . . . . . . . . . . . . . 104
4.10 Diagrama de classes do driver SPI . . . . . . . . . . . . . . . . . . . . 105
4.11 Diagrama de funcionalidades do ADEOS . . . . . . . . . . . . . . . . 115
5.1
Placa de desenvolvimento 8051DKUSB . . . . . . . . . . . . . . . . . 128
xiii
5.2
5.3
5.4
5.5
5.6
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) . . . . . . .
130
132
134
135
137
A.1 PCB spi2c: esquemático . . . . . . . . . . . . . . . . . . . . . . . . . 146
A.2 PCB spi2c: layout . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
xiv
Lista de Tabelas
2.1
Implementação da classe Shape em C++ e Java . . . . . . . . . . . .
8
3.1
3.2
3.3
3.4
3.5
3.6
3.7
Vetores de interrupção na famı́lia MCS-51 . . . . . . . . . . . . . . .
Modos de endereçamento do 8051 . . . . . . . . . . . . . . . . . . . .
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++ 8051 da IAR
Registos utilizados nos parâmetros das funções . . . . . . . . . . . . .
Registos utilizados no retorno das funções . . . . . . . . . . . . . . .
4.1
4.2
4.3
4.4
4.5
4.6
4.7
4.8
4.9
Rotina de interrupção do temporizador 1 . . . . . . . . . . . . . . . .
Configuração do temporizador 1 . . . . . . . . . . . . . . . . . . . . .
Inicialização da contagem do temporizador 1 . . . . . . . . . . . . . .
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
5.1
5.2
Caracterı́sticas de hardware da placa de desenvolvimento 8051DKUSB 128
Configuração usada no teste ao sistema operativo . . . . . . . . . . . 131
34
35
51
53
60
61
62
84
85
85
114
117
120
121
123
125
xv
Lista de Listagens
2.1
Implementação C (iterativa) do cálculo do fatorial de um número . .
6
2.2
Implementação Haskell do cálculo do fatorial de um número . . . . .
6
2.3
Definição da classe Triangle como herdeira da classe Shape . . . . . .
9
2.4
Exemplo de polimorfismo dinâmico na classe Triangle . . . . . . . . .
10
2.5
Declaração da classe Shape como abstrata . . . . . . . . . . . . . . .
11
2.6
Exemplo da utilização de compilação condicional
. . . . . . . . . . .
22
3.1
Declaração da classe Task . . . . . . . . . . . . . . . . . . . . . . . .
39
3.2
Função de iniciação das tarefas - run . . . . . . . . . . . . . . . . . .
41
3.3
Construtor da classe Task . . . . . . . . . . . . . . . . . . . . . . . .
41
3.4
Método schedule da classe Sched
. . . . . . . . . . . . . . . . . . . .
44
3.5
Construtor da classe Mutex
. . . . . . . . . . . . . . . . . . . . . . .
46
3.6
Valores em template metaprogramming . . . . . . . . . . . . . . . . .
47
3.7
Funções em template metaprogramming . . . . . . . . . . . . . . . . .
48
3.8
Saltos condicionais em template metaprogramming . . . . . . . . . . .
48
3.9
Recursividade em template metaprogramming
. . . . . . . . . . . . .
49
3.10 Implementação C++ recursiva do cálculo do fatorial . . . . . . . . . .
49
3.11 Implementação C++ TMP recursiva do cálculo do fatorial . . . . . .
50
3.12 Implementação C++ TMP de uma lista ligada estática de inteiros . .
51
3.13 Metafunção Length da lista ligada estática . . . . . . . . . . . . . . .
52
3.14 Função de interrupção de overflow do timer 0 . . . . . . . . . . . . .
57
3.15 Exemplo de utilização de inline assembler no compilador IAR . . . .
58
3.16 Definição de uma função implementada num ficheiro assembly externo
59
3.17 Estrutura de um ficheiro assembly gerado pelo compilador IAR . . . .
59
4.1
Ficheiro bsp.h para a arquitetura 80188 . . . . . . . . . . . . . . . . .
67
4.2
Protótipo da função contextInit . . . . . . . . . . . . . . . . . . . . .
68
4.3
Protótipo da função contextSwitch . . . . . . . . . . . . . . . . . . . .
73
xvii
4.4
4.5
4.6
4.7
4.8
4.9
4.10
4.11
4.12
4.13
4.14
4.15
4.16
4.17
4.18
4.19
4.20
4.21
4.22
4.23
4.24
4.25
4.26
4.27
4.28
4.29
xviii
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 Pwm8051 . . . . . . . . . . . . . . . . . . . .
Estrutura de configuração da classe Pwm8051 . . . . . . . . . . . .
Enumerações da classe Pwm8051 . . . . . . . . . . . . . . . . . . .
Método config da classe Pwm8051 . . . . . . . . . . . . . . . . . . .
Declaração da classe Uart8051 . . . . . . . . . . . . . . . . . . . . .
Construtor da classe Uart8051 com configuração . . . . . . . . . . .
Métodos txStart e rxStart da classe Uart8051 . . . . . . . . . . . .
Declaração da classe Gpio8051 . . . . . . . . . . . . . . . . . . . .
Método config da classe Gpio8051 . . . . . . . . . . . . . . . . . . .
Declaração da classe I2c051 . . . . . . . . . . . . . . . . . . . . . .
Construtor por defeito da classe I2c051 . . . . . . . . . . . . . . . .
Métodos start e write char da classe I2c8051 . . . . . . . . . . . .
Declaração da classe Spi8051 . . . . . . . . . . . . . . . . . . . . .
Construtor da classe Spi8051 com configuração . . . . . . . . . . .
Métodos read char da classe Spi8051 . . . . . . . . . . . . . . . . .
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 . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
76
76
77
85
88
89
89
90
93
94
94
97
97
101
102
102
106
107
107
111
112
113
118
119
122
124
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
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
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
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 8051.
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
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
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.
2.1.1
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
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.
2.1.2
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
2.1. Programação Orientada a Objetos
Tabela 2.1: Implementação da classe Shape em C++ e Java
Linguagem
C++
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;
};
Java
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).
2.1.3
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
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
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
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
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.
2.2.1
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
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
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.
2.2.2
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
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).
2.2.3
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
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
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
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
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
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
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 80188.
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 3.2.
2.3
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
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.
2.3.1
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
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].
2.3.2
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.
2.3.3
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
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.
2.3.4
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 fun9
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
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.
2.3.5
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
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.
2.3.6
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
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 custo27
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 3.3.
2.4
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
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 8051. 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
8051. 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 proces29
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), CAN1 , 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
Capı́tulo 3. Especificação do Sistema
3.1.1
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 8051.
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
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 8051.
Figura 3.2: Mapa de memória do 8051 genérico
3.1.2
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
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.
3.1.3
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 16bit; 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 (LEDs3 , ataque de MOSFETs4 , 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
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
3
33
3.1. Microcontrolador 8051
Tabela 3.1: Vetores de interrupção na famı́lia MCS-51
Interrupção
RESET
Externa 0
Timer 0
Externa 1
Timer 1
Porta série
Flag de Interrupção
RST
IE0
TF0
IE1
TF1
RI ou TI
Bit SFR
TCON.1
TCON.5
TCON.3
TCON.7
SCON.0 ou SCON.1
Endereço SFR
00h
03h
0Bh
13h
1Bh
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]
3.1.4
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
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
3.1.5
Código exemplo
MOV A,#55H
MOV A,50H
MOV A,R7
MOV A,@R0
PUSH ACC
MOVC @A+DPTR
SJMP loop0
ACALL loop1
LJMP loop2
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
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
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 1000. 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 80188 [54]. Portanto,
para fazer o porting do sistema operativo para a plataforma 8051, apenas devem ser
re-implementadas estas três rotinas.
37
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
3.2.1
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
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
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
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
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.
3.2.2
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
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
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
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
3.2.3
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
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
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.
3.3.1
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
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
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
3.3.2
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
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 8051. 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
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
Fatorial (C++ dinâmico)
Fatorial (TMP)
3.3.3
Tempo execução (ciclos relógio)
2578
8
Memória de código (bytes)
300
53
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
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 8051.
3.4
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
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
Código assembly
void main ()
{
typedef Cons<1,Cons<2,Cons<3,End>>> list1;
P0 = Lenght<list1>::value;
P0 = IsEmpty<list1>::value;
}
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 8051. 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
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
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;
3.4.1
Compilador IAR C/C++ para o 8051
O IAR C/C++ Compiler for 8051 é uma das ferramentas integradas na IAR Embedded Workbench for 8051. 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
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
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 8051. 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
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 MCS51 é 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
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 3.16.
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 3.17. 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
3.4. Ambiente de Desenvolvimento
Tabela 3.5: Convenções de chamada de funções no compilador C/C++ 8051 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
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
data overlay
–
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
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
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
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
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 8051. 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 MCS51
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 dedi65
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 80188 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 80188 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
Capı́tulo 4. Implementação do Sistema
4.1.1
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
8051. Além disso, vai permitir que o mesmo adquira competência e conhecimentos
relativamente à arquitetura e conjunto de instruções do 80188.
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 80188
A estrutura Context permite guardar o estado atual do processador, isto é, o
valor dos registos essenciais do 80188 utilizados por uma determinada tarefa. Neste
67
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
Capı́tulo 4. Implementação do Sistema
Algoritmo 1 Inicialização do contexto no 80188 - 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
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, 0000001000000000b; 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
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
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 80188 - 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
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
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
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
4.1.2
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 80188 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 8051.
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
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
unsigned char
unsigned char
unsigned char
unsigned char
unsigned char
unsigned char
unsigned char
};
PC H, PC L;
A, B;
IE;
DPL, DPH;
R0, R1, R2, R3, R4, R5, R6, R7;
PSW;
SP;
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
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 (216 = 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
4.1. Porting do ADEOS para a Plataforma MCS-51
Algoritmo 3 Inicialização do contexto no 8051 - 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
MOVX @DPTR, A; pContext−>PC H = pFunc H
INC DPTR; point to pContext−>PC L
MOV A, 4; A = pFunc L
MOVX @DPTR, 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
Capı́tulo 4. Implementação do Sistema
MOVX @DPTR, 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;
MOVX @DPTR, 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
MOVX @DPTR, A; pContext−>DPL = 0
INC DPTR; point to pContext−>DPH
MOVX @DPTR, A; pContext−>DPH = 0
;Initialize Registers.
INC DPTR; point to pContext−>R0
MOV A, #0;
MOVX @DPTR, A; pContext−>R0 = 0
...
;Initialize Processor Flags
PUSH PSW; Save PSW in stack
ANL PSW,#0x00; CLEAN ALL FLAGS
MOV A, PSW; A = PSW;
MOVX @DPTR, 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
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 4.1.1. 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
MOVX @DPTR, 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
Capı́tulo 4. Implementação do Sistema
Algoritmo 4 Mudança de contexto no 8051 - 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
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
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.
4.2.1
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
4.2. Upgrade do ADEOS
Tabela 4.1: Rotina de interrupção do temporizador 1
Método C++
#pragma vector = TF1 int
interrupt void Sched::tick(void)
{
enterCS();
recharge sched tick(˜CYCLES PER TICK);
os.schedule();
CDP (8051)
recharge sched tick:
CODE
MOV TL1,R2
MOV TH1,R3
RET
exitCS();
}
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
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
4.2.2
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
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 8051. 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 8051.
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
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
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
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
4.11. 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 4.10.
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
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
break;
case 1:
CCAP1H=DutyCycle; //Reload value
break;
case 2:
CCAP2H=DutyCycle; //Reload value
break;
case 3:
CCAP3H=DutyCycle; //Reload value
break;
}
}
to Duty−Cycle
to Duty−Cycle
to Duty−Cycle
to Duty−Cycle
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
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
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
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 8bit 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 4.13. 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
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
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
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
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
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
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
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
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
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 4.18.
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
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
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.
SPI DD: Design
104
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
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
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 4.21. 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
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.
4.2.3
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 processa108
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
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. fi ∈ {f1 , ... ,fm |f1 < ... <fm }
such that x ≤ fi /fm
upon task release(Ti ):
set c lef ti = Ci ;
defer();
upon task completation(Ti ):
set c lef ti = 0;
defer();
during task execution(Ti ):
decrement c lef ti ;
defer():
set U = C1 /P 1 + ... + Cn /Pn ;
set s = 0;
for i = 1 to n, Ti ∈ {T1 , ... ,Tn |D1 ≥ ... ≥Dn };
set U = U - Ci /Pi ;
set x = max(0 , c lef ti - (1-U )(Di -Dn ));
set U = U + (c lef ti -x)/(Di -Dn );
set s = s + x;
endfor
select frequency (s/(Dn - 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
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 fi representa a frequência selecionada entre as frequências
discretas disponı́veis, fm a frequência máxima, Ti representa a taref ai da lista de tarefas, Ci o WCTE da taref ai , c lef ti o tempo que falta para atingir o WCET da
taref ai , Pi o perı́odo da taref ai , Di a deadline da taref ai , 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
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
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 4.25. 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
4.3. Refactoring do ADEOS
Tabela 4.4: Implementação C++ e assembly da seleção da frequência
Método C++
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
CDP (8051)
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
Refactoring do ADEOS
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.
4.3.1
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
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
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
Capı́tulo 4. Implementação do Sistema
4.3.2
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
example2.h
class example1
{
public:
example1() {} //Constructor
˜example1() {} //Destructor
void func();
void set attr(unsigned char);
unsigned char get attr();
private:
unsigned char attr 1;
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 example1::func()
{
}
//Method example
void example2::func()
{
}
//Attribute set example
void example1::set attr(unsigned char attr)
{
attr 1 = attr;
}
//Attribute set example
void example2::set attr(unsigned int attr)
{
attr 2 = attr;
}
//Attribute get example
unsigned char example1::get attr()
{
return attr 1;
}
//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 clas117
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
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
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.
4.3.3
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
config adeos.h
#include ”sched1.h”
#include ”sched2.h”
...
template <typename SchedType> class schedManager;
typedef schedManager<sched> Sched;
sched sched obj;
/∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 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
Capı́tulo 4. Implementação do Sistema
Tabela 4.7: Definição das templates genérica e especificas da funcionalidade Sched
Template
Código Template
Generic
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∗/}
};
Sched1
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;}
};
Sched2
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
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
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
config adeos.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];
...
/∗∗∗∗∗∗∗∗ 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
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
Capı́tulo 4. Implementação do Sistema
Tabela 4.9: Definição das templates genérica e especificas da funcionalidade Task
Template
Código Template
Generic
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∗/}
};
Task1
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; }
};
Task2
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
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
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 memó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, alimentação USB, conector de 44 pinos para
expansão dos quatro portos do microcontrolador, comunicação série através da porta
USB (FTDI), display de 7-segmentos ligado ao porto 1, e programação ISP (InSystem Programming) manual ou automática. A tabela 5.1 resume as caracterı́sticas
fundamentais do microcontrolador.
Figura 5.1: Placa de desenvolvimento 8051DKUSB
Para compilar o código fonte do sistema operativo ADEOS, incluindo o código
das tarefas a executar, foi utilizado o compilador C/C++ da IAR para o 8051. Nas
opções de compilação foi definida a opção de otimização None, de modo a obter código
máquina sem qualquer otimização. Desta forma será possı́vel avaliar de forma mais
fidedigna da influência do template metprogramming nas métricas de desempenho,
128
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
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 (variabilidadef uncionalidades = 25 ) 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
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.
5.3.1
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
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
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
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)
5.3.2
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
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
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
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
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
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 8051.
140
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
Apêndices
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
Figura A.1: PCB spi2c: esquemático
146
Apêndice A. Placa Circuito Impresso: spi2c
Figura A.2: PCB spi2c: layout
147
Bibliografia
[1] D. Tennenhouse, “Proactive computing,” Communications of the ACM, pp. 43–
45, May 2000.
[2] A. McHoes and I. M. Flynn, Understanding Operating Systems, 6th ed. Course
Technology, 2010.
[3] AUTOSAR, “Requirements on operating system,” Automotive Open System
Architecture GbR, Tech. Rep., June 2006.
[4] P. J. Plauger, “Embedded c++: An overview,” Embedded Systems Programming,
1997.
[5] D. Herity, “C++ in embedded systems: Myth and reality,” EE Times India,
1998.
[6] K. Czarnecki, “Generative programming: Principles and techniques of software
engineering based on automated configuration and fragment-based component
models,” Ph.D. dissertation, University of Ilmenau, 1998.
[7] K. Czarnecki and U. Eisenecker, Generative Programming: Methods, Tools, and
Applications, 1st ed. Addison-Wesley Professional, 2000.
[8] N. Cardoso, P. Rodrigues, O. Ribeiro, J. Cabral, J. Monteiro, J. Mendes, and
A. Tavares, “An agile software product line model-driven design environment for
video surveillance systems,” September 2012.
[9] N. Cardoso, J. Vale, O. Ribeiro, J. Cabral, P. Cardoso, J. Mendes, and A. Tavares, “Model-driven template metaprogramming,” September 2012.
149
[10] N. Cardoso, J. Vale, J. Cabral, J. Mendes, P. Cardoso, A. Tavares, and J. Monteiro, “Use of template metaprogramming to address the heterogeneity of video
surveillance systems,” March 2012.
[11] N. Cardoso, J. Cabral, P. Cardoso, J. Mendes, A. Tavares, and J. Monteiro, “A
novel approach to manage the complexity and heterogeneity of video surveillance
systems,” March 2012.
[12] C. Steup, M. Schulze, and J. Kaiser, “Exploiting template-metaprogramming
for highly adaptable device drivers - a case study on canary an avr can-driver,”
in 12th Brazilian Workshop on Real-Time and Embedded Systems, 2010.
[13] D. Abrahams and A. Gurtovoy, “The boost mpl library.”
[14] B. W. Kernighan and D. M. Ritchie, C Programming Language, 2nd ed. Prentice
Hall, 1988.
[15] D. G. Alcock, Illustrating BASIC (A Simple Programming Language), 1st ed.
Cambridge University Press, 1977.
[16] S. Leestma and L. Nyhoff, Pascal Programming and Problem Solving, 4th ed.
Prentice Hall, 1993.
[17] M. A. Covington, D. Nute, and A. Vellino, Prolog Programming in Depth, 1st ed.
Prentice Hall, 1996.
[18] G. Hutton, Programming in Haskell, 1st ed. Cambridge University Press, 2007.
[19] P. Winston and B. Horn, Lisp, 3rd ed. Addison-Wesley, 1989.
[20] B. Stroustrup, C++ Programming Language, 3rd ed.
sional, 1997.
Addison-Wesley Profes-
[21] J. Smiley, Learn to Program with Java, 1st ed. Osborne/McGraw-Hill, 2002.
[22] G. G. Abraham Silberschatz, Peter Galvin, Operating System Concepts, 8th ed.
Wiley, 2008.
[23] GNU Operating System. [Online]. Available: http://www.gnu.org/
150
[24] QNX: Operating systems, development tools, and professional services for
connected embedded systems. [Online]. Available: http://www.qnx.com/
[25] D. Lewis, Fundamentals of Embedded Software: Where C and Assembly Meet,
1st ed. Prentice Hall, 2001.
[26] LynxOS RTOS: The real-time operating system for complex embedded systems.
[Online]. Available: http://www.lynuxworks.com/rtos/
[27] Using the FreeRTOS Real Time Kernel. [Online]. Available:
//www.freertos.org/
http:
[28] V. F. Russo, “An object-oriented operating system,” Ph.D. dissertation, University of Illinois at Urbain-Champaign, 1990.
[29] Choices. [Online]. Available: http://choices.cs.uiuc.edu/
[30] Trion Development Object Oriented Operating System. [Online]. Available:
http://trion.sourceforge.net/index.php
[31] F. Afonso, C. Silva, S. Montenegro, and A. Tavares, “Middleware fault tolerance
support for the boss embedded operating system,” in Aspects, Components, and
Patterns for Infrastructure Software, International Workshop on, 2007.
[32] ——, “Applying aspects to a real-time embedded operating system,” in Intelligent Solutions in Embedded Systems (WISES), International Workshop on, 2006.
[33] S. Montenegro and F. Zolzky, “Boss/evercontrol os/middleware target ultra high
dependability,” in Data Systems on Aerospace (DASIA), 2005.
[34] CERG. Embedded System Research Group . [Online]. Available:
//esrg.dei.uminho.pt/
[35] M. Barr, Programming Embedded Systems in C and C ++, 1st ed.
Media, 1999.
http:
O’Reilly
[36] Y. Hu, E. Merlo, M. Dagenais, and B. Lagüe, “C/c++ conditional compilation
analysis using symbolic execution,” 2000.
[37] G. team. GCC, the GNU Compiler Collection. [Online]. Available:
//gcc.gnu.org/
http:
151
[38] H. Spencer and G. Collyer, “]ifdef considered harmful, or portability experience
with c news,” in USENIX ’92, June 1992.
[39] D. Lohmann, F. Scheler, R. Tartler, O. Spinczyk, and W. Schröder-Preikschat,
“A quantitative analysis of aspects in the ecos kernel,” in EuroSys ’06, April
2006.
[40] M. Franz, P. Frohlich, and T. Kistler, “Towards language support for componentoriented real-time programming (position paper).”
[41] C. Prehofer, “Feature oriented programming: A fresh look at objects,” 1997.
[42] D. Batory, “A tutorial on feature oriented programming and product-lines,” in
25th International Conference on Software Engineering (ICSE’03), 2003.
[43] G. Kiczales, J. Lamping, A. Mendhekar, C. Maeda, C. V. Lopes, J.-M. Loingtier, and J. Irwin, “Aspect-oriented programming,” in European Conference on
Object-Oriented Programming (ECOOP), 1997.
[44] O. Spinczyk, A. Gal, and W. Schröder-Preikschat, “Aspectc++: An aspectoriented extension to the c++ programming language,” in 40th Internacional
Conference on Technology of Object-Oriented Languages and Systems, 2002.
[45] D. Abrahams and A. Gurtovoy, C++ Template Metaprogramming: Concepts,
Tools, and Techniques from Boost and Beyond, 1st ed. Addison-Wesley Professional, 2004.
[46] D. D. Gennaro, Advanced C++ Metaprogramming, 1st ed.
pendent Publishing Platform, 2011.
CreateSpace Inde-
[47] Intel. [Online]. Available: http://www.intel.com
[48] M. A. Mazidi, J. G. Mazidi, and R. D. McKinlay, The 8051 Microcontroller and
Embedded Systems, 2nd ed. Prentice Hall, 2005.
[49] Texas Instruments. [Online]. Available: http://www.ti.com/
[50] CC1111/CC2511 USB HW User’s Guide. [Online]. Available: http://www.ti.
com/lit/ug/swru082b/swru082b.pdf
152
[51] A True System-on-Chip Solution for 2.4-GHz IEEE 802.15.4 and ZigBee Applications. [Online]. Available: http://www.ti.com/lit/ds/swrs081b/swrs081b.pdf
[52] A. Tavares, C. Lima, C. Silva, J. Cabral, and P. Cardoso, Programação de Microcontroladores, 1st ed. Netmove Comunicação Global, Lda. Editora, 2009.
[53] ATMEL - 8051 Microcontroller Instruction Set. [Online]. Available:
//www.atmel.com/Images/doc0509.pdf
http:
[54] Intel - 80186/80188 HIGH-INTEGRATION 16-BIT MICROPROCESSORS.
[Online]. Available: http://www.ieeta.pt/∼jaf/apoio ip/praticas/data sheets/
ds80188red.pdf
[55] N. Cardoso, J. Vale, O. Ribeiro, J. Cabral, P. Cardoso, J. Mendes, and A. Tavares, “Model-driven template metaprogramming,” 2012.
[56] Atmel - FLIP. [Online]. Available: http://www.atmel.com/tools/FLIP.aspx
[57] K. C. Louden, Compiler Construction: Principles and Practice, 1st ed. Course
Technology, 1997.
[58] Ceibo Offers 8051 C++ Compiler. [Online]. Available: http://www.keil.com/
pr/article/1032.htm
[59] IAR Embedded Workbench for 8051. [Online]. Available: http://www.iar.com/
en/Products/IAR-Embedded-Workbench/8051/
[60] SILICON LABS - C8051F120/1/2/3/4/5/6/7 C8051F130/1/2/3. [Online].
Available:
http://www.silabs.com/Support%20Documents/TechnicalDocs/
C8051F12x-13x.pdf
[61] A True System-on-Chip Solution for 2.4-GHz IEEE 802.15.4/ZigBee. [Online].
Available: http://www.ti.com/lit/ds/symlink/cc2430.pdf
[62] DS80C390 Dual CAN High-Speed Microprocessor. [Online]. Available:
http://datasheets.maximintegrated.com/en/ds/DS80C390.pdf
[63] DS80C400 Network Microcontroller. [Online]. Available:
maximintegrated.com/en/ds/DS80C400.pdf
http://datasheets.
153
[64] 8051 IAR C/C++ Compiler - Reference Guide, 4th ed., IAR Systems, February
2008.
[65] Tabela de Instruções do 8086. [Online]. Available: http://dcc.ufrj.br/∼renancg/
hs/cp/uteis/INSTRUCOES 8086.pdf
[66] J. Corbet, A. Rubini, and G. Kroah-Hartman, Linux Device Drivers, 3rd ed.
O’Reilly Media, 2005.
[67] MAX232, MAX232I - DUAL EIA-232 DRIVERS/RECEIVERS. [Online].
Available: http://www.ti.com/lit/ds/symlink/max232.pdf
[68] I 2 C Bus - Technical Overview. [Online]. Available: http://www.mcc-us.com/
I2CBusTechnicalOverview.pdf
[69] AT89C51ID2 - Datasheet. [Online]. Available: http://www.atmel.com/Images/
doc4289.pdf
[70] SPI three slaves. [Online]. Available: http://upload.wikimedia.org/wikipedia/
commons/f/fc/SPI three slaves.svg
[71] SPI timing diagram. [Online]. Available:
http://upload.wikimedia.org/
wikipedia/commons/6/6b/SPI timing diagram2.svg
[72] B. Mochocki, X. S. Hu, and G. Quan, “A realistic variable voltage scheduling
model for real-time applications,” 2002.
[73] H. Aydin, R. Melhem, D. Mossé, and P. Mejı́a-Alvarez, “Dynamic and aggressive
scheduling techniques for power-aware real-time systems,” 2001.
[74] Y. Shin and K. Choi, “Power conscious fixed priority scheduling for hard realtime systems,” 1999.
[75] P. Pillai and K. G. Shin, “Real-time dynamic voltage scaling for low-power
embedded operating systems,” 2001.
[76] Y. SHIN, K. CHOI, and T. SAKURAI, “Power-conscious scheduling for real-time
embedded systems design,” 2001.
154
[77] K. C. Kang, S. G. Cohen, J. A. Hess, W. E. Novak, and A. S. Peterson, “Featureoriented domain analysis (foda) feasibility study,” Carnegie-Mellon University
Software Engineering Institute, Tech. Rep., 1990.
[78] S. Toolworks. Understand - Source Code Analysis & Metrics. [Online].
Available: http://www.scitools.com/
[79] M. Technology. Microchip. [Online]. Available: http://www.microchip.com/
[80] ——. MCP23017/MCP23S17 Datasheet: 16-Bit I/O Expander with Serial Interface. [Online]. Available: http://ww1.microchip.com/downloads/en/
devicedoc/21952b.pdf
[81] T. Instruments. ADS7834 Datasheet:
12-Bit High-Speed, Low-Power
Sampling ANALOG-TO-DIGITAL CONVERTER. [Online]. Available: http:
//www.ti.com/lit/ds/sbas098a/sbas098a.pdf
[82] ——. ADS7823 Datasheet: 12-Bit, Sampling A/D Converter with I 2 C
INTERFACE. [Online]. Available: http://www.ti.com/lit/ds/symlink/ads7823.
pdf
[83] Trion Design Proposition. [Online]. Available:
?group id=90198
http://sourceforge.net/cvs/
[84] E. Gamma, R. Helm, R. Johnson, and J. Vlissides, Design Patterns: Elements of
Reusable Object-Oriented Software, 1st ed. Addison-Wesley Professional, 1994.
155
Download

versão electrónica - Nova Impressora HP2430_2 Nova impressora