home

Como o X Real-Time Kernel pode me ajudar?

Durante os últimos anos, os microcontroladores de 32 bits trouxeram muito mais poder de computação para sistemas embarcados que anteriormente usavam apenas 8 bits. Ao mesmo tempo, as aplicações tornaram-se mais complexas, exigindo muito mais dos seus desenvolvedores.

Tipicamente, uma aplicação embarcada precisa monitorar e controlar uma grande quantidade de eventos, de forma precisa e temporizada. O início de um projeto deste tipo normalmente envolve uma questão fundamental: como estruturar o software?

O X Real-Time Kernel, ou simplesmente X Kernel, é um RTOS (Real-Time Operating System) que oferece uma solução prática e elegante para a estruturação e o projeto de um sistema embarcado. Entretanto, é comum que desenvolvedores que iniciam na área tenham dúvidas sobre seu uso e sua função dentro do sistema. Algumas perguntas freqüentes são:

  • Por que preciso de X Kernel?
  • Como o X Kernel pode me ajudar?
  • Minha aplicação ficará mais complexa? Mais cara?

Para responder a estas perguntas, um exemplo será bem ilustrativo. Este exemplo será implementado de duas formas. A primeira usando apenas a programação básica e a segunda usando o X Kernel.

O objetivo aqui é apresentar através de um exemplo simplificado que a utilização do X Kernel traz diversos benefícios como:

  • Redução do tempo de desenvolvimento do SW e conseqüente redução do Time-to-Market

  • Melhor estruturação do SW em módulos de baixa inter-dependência, facilitando muito o desenvolvimento por um grupo de programadores

  • Foco da equipe de desenvolvimento em resolver as questões ligadas à aplicação ao invés de gastar tempo (ou seja, custo e prazo) no desenvolvimento e depuração de estruturas de controle complexas, resultantes da execução seqüencial de atividades de natureza concorrente

  • Simplificação da manutenção do código pela melhor estruturação do mesmo

  • Incentivo ao reuso do código, e conseqüente redução de custo e prazo em projetos futuros, através da modularização

  • Disponibilidade do X-HAL (X Hardware Abstraction Layer - uma camada de abstração do hardware) que simplifica muito o acesso aos periféricos e o porte entre processadores


O Exemplo

Um sistema hipotético deve continuamente monitorar o estado de 3 chaves mecânicas. Um aviso deve ser enviado por um canal de comunicação sempre que uma delas mudar de posição. Para tornar o problema um pouco mais interessante, vamos adicionar algumas características:

  • As chaves estão sujeitas a repique, portanto, é necessário incluir um debounce de 20 ms. Desta forma, só se considera que a chave mudou de estado quando ela passou no mínimo 20 ms no novo estado.

  • Antes de enviar informações pelo canal de comunicação, o sistema precisa estabelecer uma conexão. Se a conexão for criada com sucesso, o sistema pode transmitir as informações. Se a conexão falhar, o sistema deve esperar 2 minutos e em seguida tentar novamente. As tentativas repetem-se até que a conexão seja estabelecida.

  • A conexão deve ser desfeita se ficar ociosa por 3 minutos, ou seja, se nenhuma chave mudar de posição durante este intervalo.

  • A monitoração das chaves deve continuar ininterruptamente, mesmo que o sistema esteja aguardando o estabelecimento de uma conexão.

Para dar ênfase às diferenças entre as duas implementações, não vamos considerar detalhes de implementação do hardware. As seguintes funções serão usadas como se estivessem definidas em uma biblioteca externa:

  • void Init(void): Inicia o hardware (interrupções, timers, canal de comunicação, etc).
  • bool OpenConnection(void): Abre a conexão (retorna false se falhar).
  • void CloseConnection(void): Fecha a conexão.
  • bool ReadKey(int key): Lê o estado da tecla "key" (retorna true ou false dependendo da posição).
  • void SendKeyState(KeyState *e): Envia o estado de uma tecla pelo canal de comunicação. A estrutura KeyState armazena o número da chave, sua posição e o instante que a chave mudou de estado. Também vamos considerar que esta função trata os eventuais erros de comunicação de forma transparente.

A implementação deste sistema usará duas “tarefas”:

    • DebounceKeys: Esta tarefa lê as chaves, faz o debounce e, quando os estados estão estáveis informa à tarefa de comunicação.

    • CommChannel: Faz o controle do canal de comunicação. Recebe os eventos de mudança de estado enviados pela tarefa DebounceKeys, abre e fecha a conexão e envia os dados.


Implementação sem o X Real-Time Kernel

Para implementar este sistema sem apoio do X Kernel, vamos utilizar a estratégia do laço principal ativado por interrupções do temporizador.

O temporizador do sistema é programado para gerar uma interrupção a cada 10 ms. A única atividade da rotina de tratamento de interrupção é atribuir o valor true à variável clock_tick.

O laço principal do programa continuamente espera que a variável clock_tick esteja em true. Quando isto ocorre, o temporizador global é incrementado (faz a função de relógio do sistema), e as funções que implementam as duas tarefas são chamadas. O valor do incremento é definido pela constante c_clock_tick_period que neste caso vale 10 ms (período da interrupção).

int main (void)
{   // Inicia o hardware
     Init();

     // Laço principal
     while (1)
     {
          // Aguarda uma interrupção do temporizador
          while (clock_tick == false);

          // Incrementa o temporizador global
          time_counter += c_clock_tick_period;

          // Dá a cada tarefa uma chance de executar
          DebounceKeys();
          CommChannel();

          // Desliga o indicador e retorna ao início do loop para aguardar a próxima interrupção
          clock_tick = false;
     }
}

A função DebounceKeys faz a leitura e o debounce das teclas. Quando uma transição é detectada, ela deve enviar esta informação para a função CommChannel. Entretanto, é preciso lembrar que o canal de comunicação pode não estar disponível quando a informação é enviada. Por esse motivo, uma fila será usada para ligar as duas tarefas e também armazenar as informações até que possam ser transmitidas. Esta fila foi implementada através de um vetor dinâmico (“vector”da biblioteca STL do C++).


void DebounceKeys (void)
{

const int c_number_of_keys = 3;

// Período de leitura das chaves (20ms)
const int c_debounce_period = 20;

// Temporizador de leitura das chaves. As chaves são lidas quando este valor chegar a 0
static int debounce_timer = c_debounce_period;

// Registrador de debounce. Cada elemento do vetor representa uma
// chave. Os 4 bits menos significativos de cada elemento
// armazenam as 4 últimas leituras das chaves.
static int key_status[c_number_of_keys];
static bool init = true;

if (init == true) {
    memset( key_status, 0, sizeof(key_status) );
    init = false;
}

// Contando o tempo para verificar as teclas
debounce_timer -= c_clock_tick_period;

// Está na hora de verificar as teclas? (1 vez a cada 20 ms)
if ( debounce_timer <= 0) {

// Reinicia o contador para a próxima verificação
debounce_timer = c_debounce_period;

// Verifica cada uma das teclas
for (int i=0; i<c_number_of_keys; i++) {

// Lê a tecla
bool key = ReadKey(i);

key_status[i] |= key;

// Verifica se a tecla mudou de estado e se o novo
// estado já estabilizou (key_status[i] contém
// "0011" ou "1100").
if ( (key_status[i] == 0x3) ||
(key_status[i] == 0xC) ) {

// Insere a tecla na fila de transmissão
KeyState e;

e.key_number = i;
e.state = key;
e.when = time_counter;
MessageQueue.push_back(e);

     }

// Deixa o registrador preparado para receber a próxima amostra
key_status[i] <<= 1;
key_status[i] &= 0xF;

     }
}

A implementação da função CommChannel exige uma máquina de estados. Dependendo da situação do canal, a função deve ter um comportamento diferente:

  • Se a conexão não foi aberta e não há mensagem na fila, deve-se esperar a recepção da primeira mensagem. Quando houver mensagem esperando na fila, deve-se abrir a conexão.

  • Se a conexão foi aberta, deve-se iniciar as transmissões. Se a conexão falhou, deve-se esperar 2 minutos e tentar novamente.

  • Após transmitir todas as mensagens que estão na fila, espera-se uma nova mensagem por no máximo 3 minutos. Se nenhuma mensagem for recebida, a conexão é fechada.


void CommChannel(void)
{

// Tempo entre tentativas de conexão (2 minutos)
const int c_connect_retry = 2 * 60 * 1000;

// Tempo máximo que a conexão fica ociosa (3 minutos)
const int c_connect_max_idle = 3 * 60 * 1000;

// Temporizador para controle da conexão
static int connection_timer = 0;

// Estado da conexão
enum eConnectionStatus { not_connected, trying, connected, waiting_try_again };
static eConnectionStatus m_constatus = not_connected;

// Máquina de estados da conexão
switch (m_constatus) {

case not_connected:

// Permanece sem conexão enquanto não houver
// mensagem para transmitir.

if (MessageQueue.empty() == false) {
     m_constatus = trying;
} break;

case trying:

// Tenta abrir uma conexão
if (OpenConnection() == true) {
     m_constatus = connected;
} else {
     // Não conseguiu conectar.
     // Aguarda para tentar novamente

     connection_timer = c_connect_retry;
     m_constatus = waiting_try_again;
}
break;

case connected:

// Conexão estabelecida
if (MessageQueue.size() != 0) {
     // Envia as mensagens que estão aguardando na fila
     for (unsigned int i=0; i<MessageQueue.size(); i++) {
         KeyState e = MessageQueue[i];
         SendKeyState(&e);
     }
     MessageQueue.clear();

     // Transmitiu todas as mensagens. Aguardando para
     // desconectar se nenhuma mensagem nova for recebida

     connection_timer = c_connect_max_idle;
} else {
     // Não há mensagens para enviar.
     // Contando o tempo da conexão ociosa.

     connection_timer -= c_clock_tick_period;
}
if ( connection_timer <= 0 ) {
     // Conexão passou muito tempo ociosa.
     CloseConnection();
     m_constatus = not_connected;
}
break;

case waiting_try_again:

// Não conseguiu conectar.
// Contando o tempo para tentar novamente

connection_timer -= c_clock_tick_period;

if (connection_timer <= 0) {
// Inicia uma nova tentativa de conexão
m_constatus = trying;
}
break;

}
}

Clique aqui para ver o código fonte completo deste exemplo.

Implementação com o X Kernel

Este mesmo exemplo implementado com auxílio do X Kernel também utiliza as duas funções DebounceKey e CommChannel. Mas, nesta nova situação, elas implementam threads do sistema operacional. Com isso, o sistema operacional pode escalonar as tarefas, ou seja, pode determinar quando é hora de cada uma delas executar.

A comunicação entre as duas threads é realizada através de um pipe, uma estrutura do X Kernel que implementa uma fila de comunicação, capaz de armazenar mensagens enviadas de uma thread para outra. No exemplo, o pipe MessageQueue conecta as duas threads, armazenando mensagens enviadas pela DebounceKey até que o canal esteja conectado e CommChannel possa transmitir.

A função main, nesta aplicação, tem apenas a função de iniciar o hardware e as threads.


int main( )
{

os.Init(); // Inicia o sistema operacional
Init();    // Inicia o hardware

// Cria a thread de debounce
TId debounce;
debounce = os.CreateThread (DebounceKeys, 0, 0, "DebounceKeys", 1024,
ARM_CODE | FIQ_ENABLE | IRQ_ENABLE, 5);

// Cria a thread do canal de comunicação
TId channel;
channel = os.CreateThread (CommChannel, 0, 0, "CommChannel", 1024,
ARM_CODE | FIQ_ENABLE | IRQ_ENABLE, 4);

// Conecta o pipe entre as duas threads (debounce -> channel)
MessageQueue.Init(debounce, channel); // Inicia o escalonamento
os.Start();

      }

Para criar uma thread no X Kernel usa-se o método CreateThread. Seus parâmetros indicam o nome da função que implementa a thread, dois valores definidos pelo programador que serão passados para a thread, o nome “legível” desta thread, o tamanho da pilha reservada para a sua execução, um conjunto de flags que informam o tipo de código (ARM ou Thumb) e habilitação de interrupções e a prioridade de execução desta thread.

A thread DebounceKeys usa os serviços do X Kernel para “adormecer” por 20 ms e, periodicamente, fazer a leitura das chaves. As mensagens com informações sobre transições nas chaves são inseridas diretamente no pipe.


void DebounceKeys (Word a, Word b)
{

// Registrador de debounce. Cada elemento do vetor representa uma
const int c_number_of_keys = 3;

// Chave. Os 4 bits menos significativos de cada elemento
// armazenam as 4 últimas leituras das chaves.

int key_status[c_number_of_keys];

memset( key_status, 0, sizeof(key_status) );

while (1) {

// A thread dorme durante 20 ms (período de leitura das chaves)
os.SleepFor(20*MSEC);

// Verifica cada uma das teclas
for (int i=0; i<c_number_of_keys; i++) {

// Lê a tecla
bool state = ReadKey(i);
key_status[i] |= state;

// Verifica se a tecla mudou de estado e se o novo
// estado já estabilizou (key_status[i] contém
// "0011" ou "1100").

if ( (key_status[i] == 0x3) || (key_status[i] == 0xC) ) {

    // Insere a tecla na fila de transmissão
    KeyState* e = new KeyState;
    e->key_number = i;
    e->state = state;
    e->when = os.GetTime();
    MessageQueue.Put(e);
}

// Deixa o registrador preparado para receber a
// próxima amostra

key_status[i] <<= 1;
key_status[i] &= 0xF;

      }

      }

      }

A thread CommChannel torna-se bem mais simples. O X Kernel permite que cada thread execute como se tivesse o processador apenas para si, sem precisar memorizar seu estado enquanto outra thread executa.


void CommChannel (Word a, Word b)
{

KeyState* e;
PutMsg msg_in_pipe;

while (1) {

// Permanece sem conexão enquanto não houver mensagem para
// transmitir. O pipe "MessageQueue" vai liberar este Receive
// quando a primeira mensagem for inserida.

os.Receive(&msg_in_pipe, sizeof(msg_in_pipe));
while (OpenConnection() == false) {
     // Não conseguiu conectar. Aguarda para tentar novamente
     os.SleepFor(2*MIN);
}

do {
     // Envia as mensagens que estão aguardando no pipe
     while (MessageQueue.IsEmpty() == false) {
          e = (KeyState *) MessageQueue.Get();
          SendKeyState(e);
          delete e;
     }
     // Aguarda uma nova mensagem ser inserida no pipe por no máximo 3 minutos.
     os.ReceiveTO (&msg_in_pipe, sizeof(msg_in_pipe), 3*MIN);
} while (msg_in_pipe.command != TIME_OUT_MSG);

// Conexão passou muito tempo ociosa (o pipe ficou vazio)
CloseConnection();

}

}

Clique aqui para ver o código fonte completo deste exemplo.

Comparações

Apesar de extremamente simples, este exemplo permite fazer comparações que são válidas para aplicações maiores. Observando as duas implementações, fica mais fácil responder as perguntas do início do artigo:

Por que preciso de um RTOS?

A medida que a aplicação fica mais complexa, envolvendo mais eventos e atividades independentes, torna-se muito difícil gerenciar “manualmente” todas as interações que existem entre estas atividades. O sistema operacional vai encarregar-se de tratar estas atividades de forma padronizada.

Como um RTOS pode me ajudar?

O sistema operacional permitiu retirar do código toda a lógica que anteriormente era necessária para controlar quando e como a aplicação deveria executar. Observando a implementação sem o sistema operacional, nota-se que esta lógica estava entrelaçada com a funcionalidade “real” da aplicação. Todo esse controle passou a ser executado pelo próprio sistema operacional e assim o programador pode focar o trabalho no desenvolvimento do seu produto.

Minha aplicação ficará mais complexa? Mais cara?

A praticidade de tratamento de temporizações e interações entre threads pode ser notada claramente na comparação entre os dois exemplos. No exemplo sem o X Kernel foi necessária uma estrutura complexa para administrar a espera pelos eventos. Com o X RTOS isto fica muito mais simples e claro. Note também que a função CommChannel tornou-se praticamente seqüencial, dispensando a técnica da máquinas de estado utilizada no primeiro exemplo.

O programa sem o X RTOS precisou de mais código para executar a mesma atividade. Na implementação com o X RTOS, a implementação das threads precisou de menos esforço de programação. Esta é exatamente a vantagem de se utilizar o sistema operacional: o código repetitivo, que pode ser reutilizado, é retirado da sua aplicação e disponibilizado em uma biblioteca para reuso. O esforço e o custo de programar esta parte comum não precisa ser repetido a cada nova aplicação.