Tenho atuado com sistemas embarcados, também conhecido como “Sistemas Embutidos” (tradução mais direta de “Embedded Systems” e, na minha opinião, mais correta), há mais de 10 anos, e sinto que esse termo é muito confuso para os jovens profissionais e recrutadores em geral.

Gostaria de ajudar com este post no esclarecimento dessas dúvidas.

Existe uma categoria geral de produtos/aplicações eletrônicos, sendo no grosso modo composto por qualquer dispositivo que deve ser ligado numa tomada ou que precisa de uma bateria para funcionar. Um sistema embarcado é uma subcategoria desse grupo. São aqueles dispositivos que possuem um sistema computadorizado inteligente internamente, que o auxilia a executar tarefas específicas e bem determinadas, capaz de retirá-lo sozinho de uma situação de erro, sem intervenção do usuário. Dessa forma o diferenciamos de um PC ou de qualquer computador de uso geral, que possuem sistemas computacionais generalizados em sua arquitetura.

Esse sistema computadorizado encontrado em sistemas embarcados é composto geralmente por microcontroladores/microprocessadores (8, 16, 32 ou 64 bits), periféricos de entrada e saída, memórias (RAM, SRAM, DDR, Flash, eMMC, SD-Card), lógica programável (FPGA, CPLD), ASICs, SoCs, etc. Dificilmente encontramos somente circuitos de lógica combinacional para tal função, mas seria teoricamente possível fazê-lo.

Dadas essas condições, posso começar a fazer comparações.

O quão diferente é programar para sistemas embarcados e desenvolver aplicativos para ambiente Desktop? No que se refere à linguagem em si, seja ela qual for, não existe diferença sintática. No entanto, o comportamento e restrições apresentados nesses ambientes podem ser ligeiramente ou consideravelmente distintos.

Um sistema embarcado apresenta restrições de hardware, seja de memória (volátil e não-volátil), seja de processamento, o que faz com que o programador precise se preocupar com o binário/bytecode gerado, de modo a utilizar o hardware de forma mais eficiente. Ao passo que num sistema Desktop tem-se  a sensação de que os recursos são “ilimitados”.

Podemos voltar no tempo e tentar encontrar o primeiro sistema embarcado que se tem notícia. Encontramos o AGC (Apollo Guidance Computer), um computador responsável pelo total controle das espaçonaves Apollo, contruídas com o objetivo de levar o homem à Lua durante os anos 60 e 70. Um detalhe muito interessante é que, embora não tivesse um microprocessador/microcontrolador internamente, tinha uma unidade computacional composta somente por lógica combinacional (portas NOR).

Um rastreador instalado no carro é um sistema embarcado? Sim! Porque possui um sistema interno computacional que executa tarefas de geolocalização, por exemplo, e outras bem definidas. Além disso faz parte de um sistema maior, o automóvel.

E um smartphone, é um sistema embarcado? Eu não o considero, o imagino como um produto eletrônico composto por diversos sistemas embarcados, tais como LCD, GPS, GPRS, Wi-Fi, etc. Enfim, uma plataforma de desenvolvimento praticamente, pois suas tarefas são extensíveis pelo usuário, simplesmente instalando novas aplicações ou imagens customizadas. Por padrão, umas das suas funções é receber e realizar chamadas.

Já trabalhei com diversos produtos eletrônicos, tais como equipamentos de controle de acesso, módulos de elevadores, computadores de bordo, rádios automotivos, terminais de consulta, relógio de ponto, etc. Todos esses são sistemas embarcados dado que possuem funções muito bem definidas e são autônomos, não necessitando de uma intervenção do usuário em caso de erro.

Vejam a definição de sistema embarcado elaborada pelo site Embarcados aqui, do qual sou um dos seus administradores. Inspirei-me muito nesse texto.

Embarcados

O que era bom ficou melhor! O Embarcados está de cara nova e, além de um ambiente mais agradável, estamos trazendo conteúdo de relevância e qualidade.

Queremos formar uma grande base de conhecimento acessível a todos e viabilizar a troca de informações e experiências que possam ajudar a crescer e desenvolver nossa capacidade como profissionais da área de sistemas embarcados.

Escreverei posts nesse portal, relacionados ao uso de plataformas open-source em sistemas embarcados, Linux embarcado, programação, e o que der vontade!

Acompanhem o portal pois tem muita gente fera lá, com assuntos muito interessantes na área de sistemas embarcados.

Compartilhem e curtam o novo Embarcados!

A biblioteca glibc, na versão 2.18, foi liberada no dia 12/08/2013. Mas o que é a glibc? Pretendo escrever outro post para descrever com mais detalhes essa resposta, mas em resumo…

Toda aplicação escrita em linguagem C, para ser executada num ambiente com ou sem sistema operacional, faz uso da biblioteca padrão C. Portanto, todo sistema Unix-like precisa de uma implementação dessa biblioteca, sendo que a mais utilizada em sistemas Linux é a glibc (GNU C Library). Existem outras implementações, dependendo da aplicação final, tais como uClibc, dietlibc, musl, Newlib, EGLIBC, etc.

Mesmo uma aplicação simples, contendo somente a instrução return 0, precisa da biblioteca padrão C? Sim! A não ser que você implemente todas as funções de bootstrap, tal como _start, as quais são chamadas pelo sistema operacional ou código de start-up ao executar o binário da aplicação em memória. Sem maiores detalhes, vamos às grandes mudanças nessa nova versão da glibc:

– Melhorado o suporte ao padrão C++11, referente ao uso de objetos do tipo thread_local;

– Melhorado o pior caso de performance das funções da biblioteca matemática libm;

– Adicionado suporte à herança de prioridade em mutex usado em pthread condition variables em arquiteturas não-x86;

– Adicionado suporte às arquiteturas Xilinx MicroBlaze e POWER8.

Todas as mudanças podem ser conferidas aqui.

Até mais,

Henrique

Como ocorre anualmente, o evento TDC2013 São Paulo (The Developer’s Conference) contou com diversas trilhas sobre assuntos diversos, tal como agile, cloud, games, testes, etc. Além dessas, foram realizadas trilhas de C/C++, Embedded e Segurança, das quais eu tive o prazer de participar.
 
Tanto a trilha de segurança quanto a trilha de embarcados foram estreantes, graças à força de vontade e entusiasmo de pessoas como Alberto Fabiano, Alan Silva e Vinicius Senger. Muito obrigado pessoal!!
 
Fiquei muito mais agradecido pois pude contribuir um pouco para a trilha de segurança, apresentando uma palestra sobre codificação segura em C para sistemas embarcados. A mesma teve o seguinte foco:
 
– Por que usar linguagem C?
– Como evitar alguns erros simples
– Padrões de codificação
– Ferramentas de análise estática e dinâmica de código
– Ataques comuns (code injection e arc injection)
– Uso de alocação dinâmica de código
 
O download da apresentação pode ser feito aqui, mas também a disponibilizei no meu slideshare.
 
Espero que este conteúdo ajude a comunidade de sistemas embarcados! Obrigado novamente aos organizadores e ao público fera desse evento, e podem contar comigo para o próximo ano!
 
Abraços,
Henrique

Introdução

O início da carreira de um desenvolvedor de software embarcado é marcado com cicatrizes de guerra. Uma delas é a definição de um microcontrolador para um novo projeto e outra é a escolha do ambiente de desenvolvimento para o mesmo. A escolha de ambos é feita quase que em conjunto, já que as ferramentas de desenvolvimento devem dar suporte para o microcontrolador especificado. Então…qual microcontrolador usar? Qual compilador deve ser escolhido? Qual é a melhor IDE? Qual a ferramenta de depuração que melhor atende o projeto? Essas são questões que atormentam os desenvolvedores e gerentes, já que quem deve assumir os custos são esses últimos.

Objetivos

As questões levantadas anteriormente são melhor entendidas e respondidas se o desenvolvedor possuir o conhecimento do funcionamento dessas ferramentas de desenvolvimento. Visando esse conhecimento, esse post explica o processo de build de um ambiente open-source, usando GNU Development Tools, para sistemas embarcados sem sistema operacional e que usem as linguagens C, C++ e Assembly.

Composição do ambiente de desenvolvimento

Um ambiente de desenvolvimento é composto de uma máquina host, onde o desenvolvimento de uma aplicação é realizado, e um dispositivo target, onde o binário da aplicação é executado. O desenvolvimento de uma aplicação que deve ser executada no próprio ambiente de desenvolvimento é chamado de desenvolvimento nativo, ao passo que o desenvolvimento de uma aplicação que deve ser executada numa plataforma diferente é chamado de desenvolvimento cross-platform.

Mas o que é uma plataforma diferente? Deve ser considerado tanto a arquitetura utilizada, ou seja, core do microcontrolador, quanto o sistema operacional que oferece o ambiente de runtime para o software final. Quando é utilizado um RTOS, geralmente os arquivos-objeto do mesmo são utilizados na geração da imagem final do firmware que deve ser gravada na memória não volátil do projeto. Portanto, o RTOS, quando utilizado dessa maneira, não influencia na distinção da plataforma. No entanto, quando um sistema operacional é utilizado, tal como o Linux ou o Windows, o mesmo é tomado como parâmetro da plataforma.

O desenvolvimento nativo, quando realizado num computador Desktop, por sua natureza, pode esconder alguns aspectos do processo de build de um software, ao passo que o desenvolvimento cross-platform para sistemas embarcados obriga o desenvolvedor a conhecer o hardware utilizado e fornecer instruções mais detalhadas às ferramentas de desenvolvimento.

Como exemplos de desenvolvimento cross-platform considere:

– Um host que utiliza a arquitetura x86 e que gera binários para um target que faz uso da arquitetura ARM9, independente dos sistemas operacionais que são utilizados por ambas as partes;

– Um host e um target que possuem a mesma arquitetura (x86, por exemplo), mas os sistemas operacionais que são utilizados em ambos ambientes são diferentes (Windows e Linux, por exemplo).

Em grande parte dos sistemas embarcados atuais não são utilizados sistemas operacionais, o que torna a distinção de plataformas a cargo somente das arquiteturas em questão.

Mas como são gerados os binários para o ambiente target no ambiente host?

Composição do GNU Toolchain

Dado que deve ser realizado um desenvolvimento cross-platform, é necessário fazer uso de um conjunto de ferramentas específico para isso, ao qual é dado o nome de cross-toolchain. Tais ferramentas trabalham em conjunto, de maneira sequencial, de forma que a saída de uma é usada como a entrada da próxima, motivo pelo qual o nome toolchain foi escolhido. A composição do GNU cross-toolchain é dada por:

– GCC (GNU C/C++ Compiler)

É o compilador responsável por converter arquivos-fonte escritos utilizando linguagens de alto nível, tais como C, C++, Java e outras, em arquivos-objeto que contêm tanto código de máquina quanto dados de programa. Ele oferece muitas opções de linha de comando e extensões de sintaxe, além de proporcionar um front-end para o GNU Linker, ld.

O gcc gera os arquivos binários no formato ELF, mas suporta outros formatos, como, por exemplo, COFF. Cada arquivo-objeto contém, em forma de seções, as informações pertinentes ao correspondente arquivo-fonte. A divisão ocorre da seguinte forma: todos os blocos de código são agrupados na seção text; todas as variáveis globais inicializadas e seus valores iniciais são reunidos na seção data; e as variáveis globais não-inicializadas são agrupadas na seção bss. Outras seções são criadas de acordo com as estruturas e linguagem utilizados, assunto sobre o qual pretendo escrever um novo post futuramente.

Basicamente, o gcc pode gerar os três tipos de arquivos-objeto estipulados pelo padrão ELF:

Relocatable file: é um arquivo-objeto que contém seções de código e de dados que pode ser usado para link-edição em conjunto com outros arquivos-objeto a fim de que ou uma imagem executável ou um shared object seja criado. Possui referências não-resolvidas, não podendo ser executado.

Executable file: é um arquivo-objeto pronto para execução, que também contém seções de código e de dados. Para um sistema bare-metal, o mesmo não possui referências não-resolvidas e já possui endereço absoluto de execução.

Shared object file: é um arquivo-objeto tal como um arquivo relocatable, no entanto este pode ser utilizado ou para a criação de novos arquivos-objeto ou para a criação de uma instância de um processo no sistema operacional, esta por meio de um linker dinâmico. Portanto, tal arquivo não tem utilidade para um sistema bare-metal.

– GNU Binutils

É um conjunto de programas para criação, manipulação e análise de arquivos binários. Desse pacote de ferramentas pode-se citar ld, as, objdump, objcopy, nm, readelf, strip, strings, gprof, etc.

O GNU Linker, ld, é responsável por combinar diversos arquivos-objeto em um maior arquivo relocatable, um shared object ou uma imagem executável final. Tal processo é guiado por um arquivo especial, o liker command file, o qual instrui o linker em como combinar os arquivos-objeto e em que posições de memória inserir tanto o código quanto os dados da imagem final.

O GNU Assembler, as, é o assembler responsável por gerar um arquivo-objeto relocatable a partir de um arquivo-fonte em Assembly.

– Biblioteca C/C++

É a biblioteca padrão das linguagens C/C++, cuja escolha deve ser feita no momento da geração do cross-toolchain, já que o compilador gcc é compilado contra uma biblioteca C específica. Existem diversas implementações da biblioteca C, tais como glibc, uClibc, eglibc, dietlibc, newlib e klibc. Para sistemas bare-metal, a biblioteca mais utilizada é a newlib, ao passo que, para sistemas que usam o Linux como sistema operacional, duas implementações são as mais utilizadas: a glibc para plataforma PC Desktop e uClibc para sistemas embarcados (Embedded Linux).

Essa biblioteca é conhecida como runtime library, já que dá suporte à execução de uma aplicação em C/C++. A mesma é implementada em dois grandes módulos: um que é a implementação do padrão ANSI C e outro que é dependente do sistema operacional utilizado.

Dentre as funções que são independentes de SO, que são parte do padrão ANSI C, pode-se citar funções de manipulação de strings, de cópia de memória, de comparação de blocos de memória, etc.

Já as funções que são dependentes do SO utilizado são chamadas de system calls. Essas funções são interfaces que, nos casos do sistema respeitar o padrão POSIX.1 (também conhecido como IEEE 1003.1), são implementadas pelo sistema operacional utilizado. As funções de gerenciamento de sistemas de arquivos, I/O, gerenciamento de memória, gerenciamento de processos, etc, são exemplos de funções que fazem parte desse grupo (write, read, open, close, sbrk, fork, getpid, execve, etc).

E quando não é utilizado um sistema operacional? Como não existe um módulo em específico que implementa tais funções, as mesmas devem ser implementadas pelo desenvolvedor. A biblioteca newlib oferece, para algumas dessas funções, stubs, os quais são implementações mínimas necessárias para que uma aplicação possa ser link-editada com a biblioteca libc.a. Para outras, como sbrk, uma implementação mínima e funcional é entregue.

Essas funções de suporte podem ser reimplementadas pelo desenvolvedor a fim de que essa interface seja realizada por meio de um agente de debug, instalado na máquina host. Esse tipo de implementação é chamado de semihosting, e é mostrada na Figura 1. Além disso, tais funções podem ser implementadas de acordo com o hardware disponível, como, por exemplo, envio/recepção de dados pela porta serial, como mostrado na Figura 2.

Figura 1 - Estrutura da biblioteca C

Figura 1 – Estrutura da biblioteca C

Figura 2 - "Retargeting" da biblioteca C

Figura 2 – “Retargeting” da biblioteca C

Um simples RTOS pode oferecer implementações de parte da biblioteca de runtime, quando necessário, tal como o gerenciamento de memória. O FreeRTOS, por exemplo, possui uma implementação desse tipo.

– GNU Build System

São ferramentas utilizadas para gerenciar pacotes de código-fonte e facilitar o processo de compilação de uma aplicação, tais como make, autotools, etc.

– GDB (GNU Debugger)

É o software depurador da toolchain GNU, que é constituído de um front-end instalado na máquina host, o qual comunica-se com um back-end no sistema target por meio um canal de comunicação tal como serial ou Ethernet.

Dado que os módulos que compõem um cross-toolchain foram resumidamente apresentados, como os mesmos interagem entre si?

Processo de build

Agora que sabemos qual é a composição do GNU toolchain, vem a pergunta? Como criar uma imagem executável que possa ser gravada na memória não-volátil (ROM, Flash, etc) de um sistema embarcado dado que possuo o código da minha aplicação? Qual é a sequência de uso dessas ferramentas? O fluxo de build de uma aplicação é mostrado na Figura 3.

Figura 3 – Processo de build em sistemas embarcados

Em linhas gerais, o desenvolvedor precisa ter em mãos os seguintes arquivos para gerar a imagem executável final:

– Aplicação

Consiste dos arquivos nas linguagens C, C++ ou Assembly (.c, .cpp, .asm, .s, .h, .hpp, etc), os quais devem ser traduzidos para a linguagem de máquina correspondente (.o).

– Bibliotecas

Caso haja necessidade, o desenvolvedor pode oferecer bibliotecas estáticas (.a) para serem compiladas na imagem final. Bibliotecas dinâmicas não podem ser utilizadas, visto que não é utilizado um sistema operacional embarcado que ofereça um linker dinâmico.

– Vetor de interrupções

Como está sendo desenvolvido um software para um microcontrolador, deve-se inicializar o seu vetor de interrupções, o qual é uma sequência de código que deve ser inserida numa região em específico da memória do mesmo. Dependendo de qual microcontrolador é utilizado, o mesmo pode ser escrito tanto em Assembly quanto em C.

– Arquivo de startup

Quando é escrito um código usando uma linguagem de alto nível, tais como C e C++, deve ser oferecido um ambiente de execução para que tais linguagens possam ser acomodadas. Cada linguagem de alto nível possui seu próprio conjunto de suposições com relação ao ambiente de execução. Para que essas expectativas sejam atendidas, seguem algumas responsabilidades desse arquivo de inicialização:

1) Inicializar a região de dados inicializados (data): copiar dos dados armazenados em ROM para RAM;

2) Inicializar a região de dados não-inicializados (bss): zerar o conteúdo dessa região;

3) Alocar espaço para a pilha e inicializar seu conteúdo;

4) Inicializar o valor do registrador de stack pointer do microcontrolador;

5) Alocar espaço para a região de heap;

6) Executar o método construtor e inicializar a vtable de cada instância global e;

7) Executar a função global main().

Deve ser lembrado que o vetor de interrupções, na maioria dos casos, pode ser inicializado pelo código de startup, em seu primeiro passo. No caso de microcontroladores baseados no core ARM Cortex-M3, o vetor de interrupções é um simples array em C, cujo arquivo binário é alocado pelo linker na posição correta, e o mesmo inicializa a pilha principal.

De forma genérica, a Figura 3 detalha a sequência dos passos necessários para a geração de um binário, seja ele executable, shared object ou relocatable. Dentro do ambiente GNU, existe uma aplicação muito útil, uma espécie de canivete suíço, que realiza todas as etapas desse processo: gcc (GNU C/C++ Compiler).

A aplicação gcc atua como um front-end para todas as etapas de build: pré-processamento, compilação C, compilação Assembly e link-edição. O mesmo chama os aplicativos necessários para cada fase. O executável gcc, na etapa de pré-processamento, analisa os arquivos-fonte e os arquivos-cabeçalho por meio do executável cpp. Assim, todos os arquivos-cabeçalho são agrupados, todas as macros são expandidas e todas as diretivas para o pré-processador são processadas. Em seguida, a compilação dos arquivos C, efetivamente, é realizada pelo executável cc1, o qual produz o código Assembly correspondente. Na sequência o executável as é chamado, o qual converte o código em Assembly gerado anteriormente e o converte para arquivo-objeto, contendo código de máquina. Por fim, os arquivos-objeto são agrupados e uma imagem executável é gerada pelo linker, papel realizado pelo executável collect2.

O simples comando abaixo gera todos os passos listados acima.

$ cross-gcc main.c startup.o vetor_de_interrupcoes.o -o main

Tanto os executáveis cc1 e collect2 são parte da distribuição do GCC. Já o executável as faz parte do GNU Binutils.

Pode-se visualizar a saída de cada etapa de compilação por meio de opções de compilação do gcc, como:

– opção de compilação -E: a saída do pré-processador é gerada ao invés de um arquivo-objeto;

– opção de compilação -S: a saída é o arquivo C convertido em Assembly;

– opção de compilação -c: a saída do compilador C, um arquivo-objeto, é gerada;

– sem as opções acima: a link-edição é executada.

Se desejar, o desenvolvedor pode fazer uso de utilitários de build, tal como make, o qual necessita de um arquivo makefile para orquestrar a geração do binário final.

Conclusões

Saber lidar com uma ferramenta como o GCC faz bem à saúde!🙂

Inauguração do blog!

Publicado: 24/05/2012 em Sobre

Olá pessoal!

Meu nome é Henrique Pérsico Rossi e sou um entusiasta da área de sistemas embarcados! Trabalho nessa área há 7 anos e já atuei com microcontroladores ARM7, ARM Cortex-M3, Renesas V850/78K0, PIC 18F/16F e quero usar muitos outros pela frente. Tenho experiência com sistemas operacionais de tempo-real (RTOS), linux embarcado e sistemas bare-metal.

Tenho a intensão de divulgar atigos nas seguintes áreas, todos envolvendo ferramentas open-source quando possível:

– Sistemas embarcados;

– Linux embarcado;

– Qualidade de software;

– Arquitetura de software (meu primeiro post!!!);

– e outros assuntos que forem surgindo na cabeça!

Basicamente é isso! Podem enviar idéias de posts e compartilhar suas opiniões, pois o lugar é para isso mesmo!

Sejam bem-vindos ao meu blog!

Henrique Pérsico Rossi

Objetivo

O objetivo deste artigo é destacar a importância da arquitetura de software de um projeto, embarcado ou não, para o desenvolvimento do mesmo, até porque esse assunto é muito extenso. O que é arquitetura de software? Para que serve? Por que requisitos não-funcionais são importantes? Responderemos a essas questões a seguir!


O que é arquitetura de software?

Pelo fato de arquitetura de software ser uma disciplina em crescimento e ainda muito jovem, não existe uma definição única para a mesma. A discussão sobre essa definição é muito extensa, já que existem diferentes pontos de vista envolvidos. As definições com as quais concordo são as seguintes:

The software architecture of a program or computing system is the structure or structures of the system, which comprise software elements, the externally visible properties of those elements, and the relationships among them.” [1]

Architecture is defined by the recommended practice as the fundamental organization of a system, embodied in its components, their relationships to each other and the environment, and the principles governing its design and evolution.” [2]

Nas definições acima são mencionados elementos, estruturas, propriedades e relacionamentos. Cada elemento possui uma implementação privada, que contém suas propriedades, e uma interface pública, que, por sua vez, propicia relacionamentos com outros elementos. E o conjunto desses elementos forma uma estrutura. Desse modo, pode-se entender a arquitetura de software como uma abstração do sistema que omite detalhes dos seus elementos, ou seja, suas partes privadas, e uma representação da relação que esses possuem entre si.

As partes privadas dos elementos não afetam como esses usam, são usados por, relacionados a ou interagem com os outros elementos do sistema. Portanto, a definição de qual estruturas de dados devem ser utilizadas e encapsuladas não é uma decisão arquitetural, ao passo que as interfaces para tais estruturas de dados faz parte da arquitetura do projeto. E pode-se dizer, também, que o comportamento de um elemento é considerado parte da arquitetura somente caso o mesmo possa ser observado ou reconhecido por outro elemento do sistema.

Dado o conceito, é interessante utilizar um exemplo de documentação de arquitetura de software. A Figura 1 auxilia nesse sentido.

Figura 1 – Exemplo de arquitetura de software

O exemplo está correto, respeitando inclusive a definição de arquitetura de software referenciada nesse artigo. No entanto, o que deve ser analisado é o que ele representa e o que não é possível concluir a partir do mesmo, tal como: a origem de cada elemento; significado da separação entre os elementos; ambiente de execução de cada elemento, podendo ser em um único ou múltiplos processadores; processamento distribuído; responsabilidades de cada elemento no sistema; significado das relações entre os elementos, podendo haver relações de uso, controle, sincronização e acesso a dados entre si; conteúdo e formato das informações que trafegam entre os elementos; etc.

Muito bem! Agora sabemos, resumidamente, o que é arquitetura. Como ela é criada, para que ela serve e como definimos suas estruturas?

Um projeto de software deve atender requisitos, os quais são divididos em dois grandes tipos: funcionais e não-funcionais ou qualidades. De forma resumida, os requisitos funcionais definem o que o sistema deve fazer, ao passo que as qualidades ditam como o sistema deve ser. Esses últimos são a base da arquitetura.

Qualidades

Qualidades, também conhecidas como atributos de qualidade, requisitos não-funcionais, requisitos não-comportamentais, etc…, são requisitos que traduzem como o sistema deve ser e não o que esse deve fazer, auxiliando na implementação das funcionalidades do sistema. Dentre as qualidades existentes, pode-se citar: usabilidade, desempenho, modificabilidade, segurança, testabilidade, etc.

Embora algumas qualidades e funcionalidades sejam intimamente ligadas, as últimas ganham muito mais foco que as primeiras na etapa de desenvolvimento, o que prejudica a visão de futuro do sistema. Por esse motivo que, dada uma modificação pedida pelo cliente, geralmente os sistemas são redesenhados, visto que os mesmos são difíceis de manter, portar para outras plataformas ou escalar, tanto horizontalmente quanto verticalmente.

Antes de serem estabelecidas tais qualidades, devem ser analisadas as restrições que são impostas à solução, as quais podem possuir origem econômica, técnica, sistêmica, política, ambiental e de planejamento e recursos, como mencionado em [3]. Tais restrições, após estudadas, podem ou não gerar requisitos para o sistema em questão.

E como são listadas as qualidades que o sistema deve possuir? Existem algumas técnicas: entrevista com o cliente; análise de negócio; análise do problema, encontrando causas raízes e soluções para o mesmo; estudo se soluções similares; etc.

A seguir seguem exemplos de declarações de requisitos funcionais, qualidades e restrições:

Requisito funcional

– O sistema deve receber/enviar requisições via comunicação de rede (Ethernet ou Wi-Fi) de/para um terminal servidor.

Restrições do sistema

Técnica: O código da solução deve ser implementado nas linguagens C ou C++ pois são as linguagens dominadas pelos desenvolvedores da equipe;

Sistêmica: A solução deve manter compatibilidade com a solução existente, que faz uso do protocolo XYZ para comunicação entre um terminal cliente e um terminal servidor;

Econômica: Não é permitido gastar acima de R$5.000,00 em licenças de software.

Requisitos não-funcionais ou qualidades

Desempenho: O sistema deve ser capaz de tratar, no mínimo, 10 requisições originadas pelo terminal servidor dentro de um segundo;

Modificabilidade: O sistema deve ser fácil de ser modificado a fim de atender mudanças futuras;

Disponibilidade: O sistema deve responder 100% das requisições efetuadas pelo servidor.

Muito bom pessoal! Até agora sabemos que é importante definir as qualidades do sistema. Só que como essas são testadas? Para isso são usados cenários de qualidade, que são compostos por seis partes:

1) Fonte do estímulo: é uma entidade, seja um humano ou um sistema computacional, que gera um estímulo;

2) Estímulo: é um evento que deve ser processado pelo sistema;

3) Ambiente: é o conjunto de condições sob o qual o estímulo ocorreu, como, por exemplo, estado normal de operação, estado de erro ativo, etc;

4) Artefato: o que é afetado pelo estímulo, podendo ser todo o sistema ou parte do mesmo;

5) Resposta: é a resposta ao estímulo;

6) Medida da resposta: é a maneira pela qual a resposta é medida de modo que a qualidade possa ser testada.

A partir do momento que é possível listar cenários de qualidade reais para um sistema, esses podem ser usados como requisitos dos mesmos. Muito bom! Vamos criar, então, um cenário de modificabilidade, já que trata-se de uma qualidade difícil de ser testada? Mãos à obra!

Na listagem de qualidades feita anteriormente foi dada a descrição de modificabilidade para o sistema exemplo. Mas dizer que deve ser fácil alterar um sistema é relativo. O que é ser “fácil”?

Modificabilidade é determinada por dois fatores: arquiteturais (como as funcionalidades são decompostas) e não-arquiteturais (técnicas de design e implementação utilizadas). Tendo em vista tais fatores, deve-se atentar à necessidade de fazer com que mudanças afetem o menor número possível de elementos do sistema, e, principalmente, deve ser levado em consideração o tempo necessário para que as mesmas sejam implementadas. Portanto, podem ser considerados níveis de complexidade para mudanças e, para cada nível especificado, um tempo de implementação deve ser estipulado.

Desse modo, pode-se definir diversos cenários de modificabilidade reais, os quais comprovam que um sistema é ou não modificável e impõe um fim à implementação da qualidade no caso de sucesso ser obtido em todos os cenários de teste. Segue um exemplo:

1) Fonte do estímulo: desenvolvedor de software.

2) Estímulo: mudança de complexidade média na API do módulo Display.

3) Ambiente: em tempo de desenvolvimento.

4) Artefato: código fonte.

5) Resposta: implementações realizadas sem causar efeitos colaterais nos outros módulos ou na arquitetura.

6) Medida da resposta: modificação finalizada em até 8 horas.

São considerados os seguintes níveis de complexidade:

– baixa: mudanças realizadas em até 4 horas;

– média: mudanças realizadas em até 8 horas;

– alta: mudanças realizadas em até 21 horas.

Agora sabemos que dada uma qualidade, sabemos como essa pode ser testada. Mas como ela é atingida? Por meio de táticas e padrões arquiteturais!

Táticas e padrões arquiteturais

Táticas são decisões arquiteturais que influenciam a resposta dada por um sistema com relação à uma ou mais qualidades. Como exemplos de táticas para disponibilidade pode-se citar detecção de falhas (ping/echo, heartbeat e tratamento de exceções), já para modificabilidade pode-se usar modificações localizadas (coerência semântica, antecipação de mudanças esperadas e generalização de módulos) e redução de efeitos colaterais (ocultação de informação e comprometimento com as interfaces existentes).

Além de táticas, podem ser utilizados padrões arquiteturais, os quais implementam um pacote de táticas já conhecidas. Esses são, em sua essência, frameworks arquiteturais, descritos por meio de um conjunto de componentes computacionais, ou simplesmente componentes, e as interações que esses realizam entre si, denominadas conectores. De modo visual, pode-se entender a arquitetura de software de um sistema como um grafo no qual seus vértices representam os componentes e suas arestas interpretam os conectores. Tais conectores podem representar chamadas à funções, eventos, queries de banco de dados, pipes, etc.

Desse modo, um padrão arquitetural define:

– um conjunto de elementos (componentes);

– uma topologia estrutural de seus elementos;

– um conjunto de mecanismos de interações (conectores) e;

– um conjunto de restrições semânticas para seu uso.

Seguem alguns dos padrões arquiteturais existentes:

pipes e filtros;

– abstração de dados e organização orientada a objetos;

– invocação orientada a eventos;

– orientação a componentes;

– funcional;

– declarativo;

– sistemas em camadas;

– repositórios e blackboards;

– interpretadores;

– sistemas distribuídos;

– arquiteturas de software para domínio específico;

– MVC.

Um dos padrões arquiteturais que é muito utilizado em sistemas embarcados é o de sistema em camadas. Um sistema desse tipo é organizado hierarquicamente, de modo que cada camada ofereça serviços para a camada acima e atue como um cliente para a camada logo abaixo. Geralmente as interações entre as camadas são realizadas por meio de chamadas de funções, como mostrado na Figura 2.

Figura 2 – Padrão arquitetural de sistemas em camadas

Portanto, uma das atividades iniciais de um arquiteto de software ao iniciar um projeto é, após entender as qualidades que o sistema deve apresentar, escolher o padrão arquitetural que melhor se adapta ao sistema em questão. Um dos aspectos mais importantes de padrões arquiteturais é que esses já apresentam soluções para requisitos não-funcionais já conhecidos.

Os relacionamentos entre os elementos de uma arquitetura podem ser reproduzidos por meio de estruturas.

Estruturas arquiteturais

Os sistemas de software atuais são muito complexos para serem entendidos de uma única vez. Para isso, é necessário entender a função de cada estrutura ou conjunto de estruturas do software. Uma comparação pode ser feita ao corpo humano. Existem médicos para cada especialização, tal como cardiologia, neurologia, obstetrícia, etc…, no entanto o corpo humano é único. Cada um desses especialistas utiliza uma visão em especial do corpo humano para realizar o seu trabalho, ao invés de inspecionar o corpo como um todo.

Mas o que são essas estruturas? São conjuntos de elementos, sejam de software, sejam de hardware, que, em conjunto, comunicam a organização arquitetural do projeto de software. Basicamente, é possível dividir tais estruturas em três grandes grupos:

1) Estruturas de módulos: É uma forma de representar os elementos da arquitetura, que neste caso são especificados como módulos, de forma estática. A esses módulos são atribuídas responsabilidades funcionais, o que permite distinguir a relação de uso entre cada elemento do sistema e, portanto, o acoplamento dos módulos entre si. Exemplos: Estruturas de decomposição, uso, camada e classe.

2) Estruturas de componente e conector: É um modo de representação dinâmica do sistema, onde cada componente é um elemento computacional e um conector é uma via de comunicação entre componentes. Ajuda a encontrar o caminho percorrido pelos dados, processos concorrentes e paralelos, etc. Exemplos: Estruturas de comunicação entre processos, concorrência, repositório e cliente-servidor.

3) Estruturas de alocação: É a representação da relação entre os elementos de software com os elementos do ambiente externo no qual o software está inserido. Auxilia na alocação de processos por processador do computador, na distribuição das tarefas de cada elemento de software entre a equipe de desenvolvimento, etc. Exemplos: Estruturas de implantação, implementação e distribuição de trabalho.

Agora você me pergunta…qual estrutura utilizar? Não existe uma resposta igual para todos os projetos, mas sim aquela que melhor se encaixa num projeto em específico. Um artigo sobre o assunto pode ser encontrado em [4]. Não tem sentido documentar uma estrutura de cliente-servidor (do tipo componente e conector) quando o padrão de repositório for utilizado no projeto, já que a troca de informações entre seus módulos é realizada por meio de um repositório de dados centralizado. O importante é notar que uma estrutura não é a arquitetura em si, mas sim um modo de visualizar como os seus elementos interagem entre si.

Por exemplo, o diagrama de uso é de grande necessidade quando o padrão arquitetural de repositório é utilizado. Esse padrão impõe que sejam especificados módulos com responsabilidades bem definidas e que esses produzam e/ou consumam dados de um repositório de dados centralizado. A estrutura de decomposição, como mostrado na Figura 3, especifica somente os módulos do sistema e indica que os mesmos produzem ou consomem dados do repositório. No entanto, falta um detalhe: como é verificado o fluxo de dados no sistema, desde a produção até o consumo? A estrutura de uso auxilia nessa função! Na Figura 4 é demonstrado o uso dos módulos entre si. Como exemplo, o módulo Compras consome os dados produzidos pelo módulo Produtos (o sentido da seta indica o sentido de uso), ao passo que o módulo Log armazena no repositório de dados informações de log geradas por todos os módulos do sistema, tal como erros ocorridos ao longo de um processo de compra ou cadastro de usuário.

Figura 3 – Estrutura de decomposição de um sistema que utiliza o padrão de repositório

Concluindo, por que arquitetura de software é importante? Pode-se listar algumas razões técnicas:

– criação de uma abstração do sistema que proporciona um meio de comunicação entre os interessados no projeto ou stakeholders;

– definição das decisões iniciais do projeto, as quais contribuem para o seu desenvolvimento e manutenção futura;

– promoção do reuso de arquitetura, já que a mesma, por trata-se de uma abstração de um sistema, pode ser reutilizada em outros sistemas que necessitem exibir os mesmos atributos de qualidade e comportamentos funcionais.

Figura 4 – Estrutura de uso de um sistema que utiliza o padrão de repositório

Desse modo concluímos esse artigo e teremos novos sobre esse assunto em breve!

Bibliografia

[1] BASS, L.; CLEMENTS, P.; KAZMAN, R. Software Architecture in Practice. 2nd ed. [S.l]: Addison-Wesley Professional, 2003.

[2] ANSI/IEEE Std 1471-2000, Recommended Practice for Architectural Description of Software-Intensive Systems

[3] LEFFINGWELL, D.; WIDRIG, D. Managing Software Requirements: A Unified Approach. [S.l]: Addison-Wesley Professional, 1999.

[4] Kruchten P. (1995), Architectural Blueprints—The “4+1” View Model of Software Architecture, [Online], Disponível em: http://www.cs.ubc.ca/~gregor/teaching/papers/4+1view-architecture.pdf [14 de Maio de 2012]