Agora que já ficou claro como lidar com o salvamento de contexto no caso de uma interrupção (leia o post III para refrescar a sua memória), é hora de falar sobre as tarefas. Um sistema com o objetivo de executar várias tarefas precisa, de alguma forma, controlá-las. É comum se usar uma estrutura conhecida como TCB (Task Control Block) com os dados mais importantes para manter as tarefas do sistema.
No BRTOS, foi usada a seguinte estrutura, cujos elementos estão descritos mais abaixo.
typedef struct {
pfTaskEntry pfEntryPoint; /* task entry point */
unsigned char ucPriority; /* task priority */
unsigned char ucTaskState; /* current task state */
unsigned short usTimeSlice; /* desired time slice */
unsigned short *pusStackBeg; /* stack beginning */
unsigned short *pusStackPtr; /* stack pointer */
unsigned short usSleepTicks; /* count sleep ticks */
unsigned short usTicks; /* count slice ticks */
} BRTOS_TCB;
- pfEntryPoint: ponto de entrada da tarefa, isto é, o endereço da função que representa a tarefa.
- ucPriority: prioridade da tarefa (não usada nesta implementação ainda).
- ucTaskState: estado atual da tarefa (em execução, dormindo, etc), importante para o escalonador.
- usTimeSlice: time slice da tarefa, ou seja, o tempo que a tarefa ficará em execução antes de perder o controle do processador para a próxima tarefa. Medido em ticks (ver post II).
- pusStackBeg: ponteiro para o início da pilha da tarefa. Cada tarefa irá necessitar de uma pilha própria para que possa executar de forma indepentente. Um pouco de memória precisará ser reservado para cada pilha sendo que a quantidade depende do código da tarefa. Um erro neste dimensionamento gera um estouro da pilha e provavelmente o sistema terá um problema grave ao invadir áreas de memória de outras tarefas ou do sistema operacional.
- pusStackPtr: ponteiro para o valor corrente da pilha da tarefa. Este ponteiro será usado para salvar a posição da pilha enquanto a tarefa espera por espaço no processador. Ao voltar a ser executada, o valor deste ponteiro é copiado no SP (stack pointer), na operação de restauração de contexto.
- usSleepTicks: este campo é usado como contador do tempo decrescente (em ticks) quando a tarefa decide “dormir” por um determinado período.
- usTicks: contador do número de ticks durante a execução da tarefa, usado para que a execução da tarefa seja exatamente do valor do time slice planejado.
Em geral, uma lista duplamente ligada de elementos BRTOS_TCB manteria o controle das tarefas. Mas, para simplificar, foi criado um pequeno vetor com BRTOS_MAX_TASKS posições (cinco apenas, nesta implementação).
Finalmente, para criar uma tarefa, basta definir a função que a represente, reservar memória para a pilha e fazer uma chamada para a função BRTOS_CreateTask().
Num microcontrolador com 1Kb de RAM e 8Kb de Flash, por exemplo, não temos o luxo de chamadas como malloc() e a reserva de espaço para pilha pode ser feita através do uso de um vetor global, do tamanho desejado. Abaixo, reservamos 50 bytes para a pilha. Note que foi usado um vetor com elementos de 16 bits (short) e não de 8 bits (char), com tamanho igual à metade do espaço desejado para fazer a reserva adequadamente. Parece estranho mas isto irá garantir que o vetor esteja alinhado na memória em valores múltiplos de 16 bits em plataformas onde o alinhamento é importante, como ARM. No ARM, isto pode significar resetar ou não.
#define TASK_STACK_SIZE 50
unsigned short usStack[TASK_STACK_SIZE/2];
A tarefa, por sua vez, é apenas um laço infinito que executa a sua operação:
void task(unsigned long ulArgc)
{
int i = 0;
while(1)
{
while(i < 50)
{
i = i + 1;
}
BRTOS_Sleep(5);
}
}
No Basic RTOS, todas as tarefas devem ser criadas dentro da chamada BRTOS_Application_Initialize(). Não foi previsto a criação de tarefas dinamicamente, uma restrição relativamente sensata quando se pensa num sistema microprocessado e realmente pequeno.
void BRTOS_Application_Initialize(void)
{
BRTOS_CreateTask(task, /* ponto de entrada*/
(unsigned short)&usStack[TASK_STACK_SIZE/2-1],/* pilha (aponte p/ o fim do vetor) */
10, /* time slice da tarefa, em ticks */
BRTOS_TASK_PRIORITY_1); /* prioridade */
}
Em geral, não se espera que a tarefa retorne mas, se isto acontecer, o sistema operacional precisa lidar com esta condição. Para isso, no momento da criação da tarefa, é gerado um contexto especial na pilha da tarefa que, caso aconteça um retorno, este a coloque em um estado seguro.
Agora sim já temos conhecimento suficiente para encarar a troca de contexto, escalonar e a criação do contexto inicial da tarefa, assunto para os próximos posts. Até lá !