Multitarefa Cooperativa em Sistemas Microcontrolados
Neste artigo Otávio Alcântara aborda a estrutura básica necessária para a implementação de um sistema cooperativo de tarefas em dispositivos com recursos limitados.
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:
preemptivos e cooperativos. 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ódulo | Responsabilidades |
|---|---|
| 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.

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.

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.
Otávio Alcântara
Otávio Alcântara é Tecnólogo em Telemática pelo CEFET-CE e especializado em desenvolvimento de software em tempo real para sistemas embutidos .

