Sistemas microcontrolados estão sendo cada vez mais aplicados no controle de sistemas complexos com restrições críticas de tempo e alto grau de paralelismo. Esses requisitos forçam que a abordagem de projeto de firmware seja mais sofisticada, com recursos de programação multitarefa.

Entretanto, os recursos de memória dos microcontroladores são restritos, o que impede o uso de um sistema operacional completo. Neste artigo iremos abordar um método de projeto de firmware que auxilia os desenvolvedores a adicionar recursos de multitarefa em seus sistemas, sem perder muito espaço de memória e recursos dos periféricos.

 

Multitarefa é a habilidade do sistema operacional de manipular várias atividades com deadlines específicos. É fazer com que as tarefas do sistema “pensem” que estão executando ao mesmo tempo. Os sistemas operacionais modernos utilizam esse mecanismo, dessa forma podemos executar vários aplicativos ao mesmo tempo.

Essa técnica compartilha o tempo de processamento da CPU entre diferentes tarefas, o algoritmo que define as regras desse compartilhamento é chamado algoritmo de escalonamento e o programa que o executa é o escalonador. O escalonador é parte integrante do kernel do sistema operacional.
Podemos dividir os algoritmos de escalonamento em duas categorias: preemptivoscooperativos. O escalonamento preemptivo define qual tarefa deve possuir a CPU e até quando. Dessa forma, a tarefa em execução pode ser interrompida pelo escalonador e outra colocada em seu lugar. Existem vários algoritmos preemptivos que definem quais são as regras de preempção das tarefas.

No escalonamento cooperativo as tarefas cedem o seu tempo de processamento quando não podem prosseguir na execução. Este algoritmo é mais simples de ser implementado, mas as interações entre as tarefas devem ser bem elaboradas para não causar latência na execução e um possível crash do sistema.  Os algoritmos descritos fazem uso de um escalonador para controlar as interações de escolha e troca das tarefas. Em um sistema microcontrolado nem sempre é possível ter espaço de código o suficiente para codificar um escalonador, mesmo que simples, pode custar alguns recursos importantes como timers e memória.

Uma forma de se contornar esse problema é fazer que a própria estrutura da aplicação cuide de “escalonar” as tarefas que serão executadas, as tarefas são dispostas num grande loop e vão sendo acessadas a cada iteração do laço. Essa abordagem aparentemente simples pode ser poderosa, se bem elaborada.

O primeiro cuidado é na modelagem do sistema, devemos está preocupados em decompor o problema em módulos independentes que tenham responsabilidades específicas. Cada módulo deve ser descrito juntamente com suas responsabilidades, tendo cuidado em separar o core do módulo das possíveis bibliotecas que este precise.

Podemos pensar nos módulos como sendo um conjunto de tarefas, estruturas de dados e rotinas co-relacionadas que trabalham para atender as responsabilidades que foram designadas pelo projetista.  Cada módulo tem uma rotina principal que chamamos de handlers; estas rotinas são as tarefas que ficam dispostas no loop principal da aplicação. Os handlers são responsáveis por “escalonar” as diferentes tarefas do módulo.

Vale ressaltar que as tarefas devem ser programadas de forma que não existam laços de espera parados (poolings) por eventos, toda vez que uma tarefa não poder prosseguir a execução, esta deve ceder o seu lugar no processador para a próxima. Em um sistema operacional essa troca é realizada pelo escalonador que cuida para que o contexto da tarefa seja salvo, para que está possa retornar a execução de onde parou na próxima iteração.

Nossa abordagem não conta com este serviço; ele deve ser programado pelo próprio desenvolvedor. Dessa forma, cada tarefa deve ser programada como uma máquina de estados, para que quando ela retome a execução prossiga do último estado que estava. Em sistemas multitarefa a interação entre as tarefas é crucial para que o sistema alcance os seus objetivos. Isto implica que precisaremos de mecanismos de comunicação entre as tarefas para sincronização e troca de mensagens.

O mecanismo mais comum são as regiões compartilhadas de memória. Como não temos a preempção das tarefas, os problemas de acesso a regiões compartilhadas de memória não aparecem. Mas é possível que ajam deadlocks, caso as interações não sejam bem desenhadas.

Iremos analisar um pequeno sistema microcontrolado que pode se beneficiar com a abordagem proposta. Os exemplos de código foram escritos em C para os microcontroladores da família MSP430 da Texas Instruments utilizando o Code Composer Essentials.

O sistema em questão trata-se de uma estação de meteorológica que coleta dados de uma rede de sensores e os transmite via modem GPRS. Os dados são mantidos em log numa memória FLASH serial, o dispositivo ainda conta com uma porta serial RS-232, display e teclado para fazer a interface local com usuários.  A rede de sensores é um barramento onde vários sensores são espetados, a estação funciona como mestre do barramento. Existe um protocolo padrão de comunicação com os diversos tipos de sensores.

A função do dispositivo é gerenciar a coleta de dados dos sensores, realizar o armazenamento desses dados e transmiti-los periodicamente para uma base central de processamento de dados. A interface local é para realização de testes e download do log.


A tabela abaixo lista os principais recursos do dispositivo.

recursos
Porta Serial
Memória Flash Serial
Display Gráfico
Teclado Alfa-númerico
Rede de Sensores
Modem GPRS
RTC

Para facilitar a análise não iremos impor nenhuma restrição temporal ou de funcionalidades mais complexas. Enfocaremos apenas a estruturação dos módulos com o intuito de criar um ambiente multitarefa cooperativo. Com as informações acima descritas, formulamos um modelo de relacionamento dos módulos.  A tabela abaixo relaciona os módulos com suas responsabilidades.

móduloresponsabilidades
Gerente de Sensores Realizar a comunicação com os módulos de sensoriamento, monitoramento do estado dos sensores, coleta periódico dos dados.
Comunicação Remota Gerenciamento do envio periódico dos logs de dados para a base central, controle e manutenção do link GPRS.
Sistema de Log Controle do armazenamento dos registros dos dados dos sensores.
Interface Local Gerencia o recebimento de comandos via teclado e controla as telas do display.
Comunicação Local Gerencia o recebimento de comandos pela interface serial, download de log de dados.


Com estas informações já podemos partir para estruturar o código. Cada módulo se transforma em um arquivo .c, onde vão ser implementas as tarefas. Os serviços dos módulos são declarados nos arquivos de cabeçalho.

Rotinas específicas de cada módulo e suas variáveis devem ser declaradas como static, dessa forma o linker não as deixará disponíveis para acesso externo.  É claro que para definir melhor quais são as tarefas de cada módulo, suas interfaces e dependências seriam necessárias mais iterações de análise do projeto. Neste artigo iremos focar apenas em como implementar rotinas em um sistema cooperativo. Numa próxima oportunidade poderemos nos aprofundar mais nas questões de modelagem do projeto.


http://www.eletronica.org/arquivos/multi_tarefa1.jpg

Ilustração 1- Rotina Principal da Aplicação


A ilustração 1 mostra um snapshot da rotina principal da aplicação, onde podemos ver os handlers de cada módulos serem chamados. Ao lado de cada chamada de método existe um comentário com o tempo máximo e mínimo que cada gerente precisa para executar. Essa informação é valiosa quando se trabalha com sistemas que precisem ser determinísticos para responder a tempo os eventos do ambiente.  O tempo de execução do loop principal deve ser menor do que o período do evento mais rápido do sistema.

Caso o sistema trabalhe com eventos assíncronos é mais adequado tratá-los usando interrupções de hardware.

O tempo de execução de cada módulo pode ser medido em tempo-real com ajuda de um pino de saída e um osciloscópio.

http://www.eletronica.org/arquivos/multi_tarefa2.jpg

Ilustração 2 - Rotina Recepção de Pacotes


A ilustração 2 é um trecho da rotina que trata a recepção dos pacotes de comando pela interface serial no módulo Gerente de Comunicação Local.

Na linha 41 do exemplo é testada a condição de um flag que sinaliza se o buffer de recepção serial está vazio. Esse teste é realizado porque a rotina de leitura da serial (readByteSerial ) é bloqueante, ou seja, espera até que exista um byte no buffer da serial.

Como nosso exemplo executa em um sistema cooperativo, fazer que o módulo fique esperando por um evento pode atrasar a execução dos outros módulos do sistema.

Ademais, podemos perceber que a rotina de recepção está estruturada como uma máquina de estados. A variável estadoRecvSerial indica qual é o estado atual da execução da rotina; dessa forma da próxima vez que a rotina for chamada ela irá executar do ponto que foi deixado antes.

Esse estilo de programação é essencial para o sucesso dessa abordagem de firmware. Todas as rotinas que demorem muito tempo para executar devem ser divididas em rotinas menores e sua execução controlada por máquinas de estado. Pode-se ter dificuldade em adicionar novas funcionalidades ao sistema pronto, principalmente por ser preciso re-verificar as restrições de tempo do sistema.

 

Neste artigo discutimos um pouco como sobre desenhar um sistema micro-controlado multi-tarefa cooperativo. Acreditamos que a solução apresentada seja prática para pequenos e médios sistemas que não possuam memória suficiente para usar um sistema operacional / escalonador de tarefas. O desenvolvedor tem a responsabilidade de estruturar as tarefas de modo que todas tenham tempo de executar e atendam os requisitos funcionais e temporais do sistema.

 

Powered by Web Agency

.
.

Copyright © MedTec do Brasil. Todos os Direitos Reservados.