Agora que já temos uma ideia geral de como funciona a comutação de tarefas num sistema multitarefa, vamos tratar do nosso pequeno comutador de tarefas e da sua implementação no RL78.
A linha RL78 da Renesas é derivada da linha 78K que, por sua vez, deriva do precursor da microcomputação em geral, o Intel 8080.
A arquitetura dos RL78 inclui um conjunto de registradores de uso geral de 8 bits (A, X, B, C, D, E, H e L) que podem ser agrupados para formar registradores de 16 bits: AX, BC, DE e HL (de maneira geral o AX atua como acumulador e os registradores BC, DE e HL podem atuar como apontadores). Além disso, estão disponíveis até 4 bancos de registradores, resultando num total de 32 registradores de 8 bits ou 16 registradores de 16 bits.
Além dos registradores de uso geral, encontramos também um registrador de estado (PSW, de 8 bits), um apontador de pilha (SP, de 16 bits), dois registradores de segmentação de memória (ES para dados e CS para programa) e um contador de programa (PC, de 20 bits). Todos esses registradores (e outros) estão mapeados na memória, exceto o PC.
A arquitetura permite até 1MB de endereçamento de memória e predispõe os últimos 64KB de memória para a área de RAM e de registradores.
No que diz respeito ao nosso agendador/comutador de tarefas, o grande número de registradores significa igual quantidade de RAM para armazenamento do contexto da tarefa e um ligeiro impacto no tempo necessário para troca de contexto, já que quanto maior o número de registradores, mais tempo se gasta empilhando e desempilhando os mesmos! Felizmente este tempo não é muito elevado graças à arquitetura CISC com um set de instruções bastante otimizado e rápido, onde uma boa parcela das instruções possui tamanho de um a dois bytes e são executadas em um ciclo de clock (graças a um pipeline de 3 estágios)!
A base do nosso agendador/comutador de tarefas consiste em realizar duas operações essenciais: o salvamento do contexto da tarefa que está sendo paralisada e a recuperação do contexto da tarefa cuja execução será retomada. O salvamento de contexto utiliza instruções assembly PUSH que permitem salvar registradores na pilha (apontada por SP) e a recuperação de contexto utiliza instruções POP que retiram valores da pilha apontada por SP. Uma observação importante: nos RL78 a pilha é decrescente, ou seja, a cada empilhamento (PUSH) o SP é decrementado em 2 e a cada desempilhamento (POP) o SP é incrementado em 2!
Por padrão, o compilador GCC utiliza apenas os três primeiros bancos de registradores para os programas em C, reservando o último para o tratamento de interrupções. Sendo assim, o nosso código de salvamento e recuperação de contexto deverá preservar apenas os três primeiros bancos e poderá fazer uso dos registradores do banco 3 para as operações internas.
A seguir temos o código do nosso agendador de tarefas round-robin. O agendador utiliza a interrupção do timer de intervalo (IT) disponível em todos os RL78. O timer é configurado para gerar uma interrupção a cada 1ms, sendo este o heart-beat do ULWOS. Isto significa que a cada 1ms esta interrupção é disparada, o PC e o PSW são salvos na pilha (da tarefa) e a execução desvia para a função INT_IT().
Dentro da função de tratamento de interrupção ocorrem três operações: o salvamento do contexto (função save_context), o avanço do apontador de tarefa (ulwos_current_task) para a próxima tarefa válida e a recuperação do contexto desta tarefa. Ao final da ISR a pilha atual é a da tarefa a ser executada, os três primeiros bancos de registradores estão com os seus valores recuperados (o contexto anterior da tarefa), então a execução da instrução assembly RETI desempilha o PC e PSW da pilha e o código da nova tarefa passa a ser executado de onde havia parado anteriormente!
void __attribute__ ((naked)) INT_IT(void) {
save_context(); // salva o contexto atual
ulwos_current_task++; // avança para a próxima tarefa na fila
// se for a última tarefa, retorna para a primeira
if (ulwos_current_task>=ulwos_num_tasks) ulwos_current_task=0;
restore_context(); // recupera o contexto da tarefa a executar
}
Note que a função INT_IT é declarada com o atributo naked. Este atributo informa ao compilador GCC que ele não deve gerar código de entrada (prólogo) e nem de saída (epílogo) para a função. Apesar da observação do manual do GCC não recomendar a utilização de extended inline assembly ou mistura de assembly e C dentro da função, acredito que esta recomendação não pode ser aplicada ao presente caso, pois todos os cuidados com salvamento e recuperação de registradores foram tomados. Além disso, o código da ISR utiliza (como veremos a seguir) apenas o banco de registradores número 3 e o simples fato de ser uma ISR já separa esta função das funções normais chamadas por software.
Observando a simplicidade do código do agendador é fácil perceber que toda a “mágica” é feita pelas funções de salvamento e recuperação de contexto. Então que tal darmos uma olhada na operação das mesmas? Mas antes de seguir, é importante conhecer as estruturas de dados utilizadas para armazenamento do contexto. O ULWOS utiliza três estruturas básicas para armazenamento do estado e contexto das tarefas:
- ulwos_task_context é um array bidimensional que armazena o conteúdo dos registradores da CPU (AX, BC, DE e HL dos bancos 0, 1 e 2);
- o array ulwos_taskSP armazena o conteúdo do stack pointer de cada tarefa e;
- o array ulwos_task_stack é um array bidimensional que armazena a pilha de memória de cada tarefa.
Atualmente o ULWOS não preserva o estado dos registradores ES e CS, mas isso pode ser facilmente modificado caso necessário!
O salvamento de contexto consiste basicamente em:
- salvar o conteúdo do SP e;
- salvar o contexto.
A pilha da tarefa não necessita ser salva pois cada tarefa possui a sua pilha individual.
Infelizmente o salvamento do SP e do contexto não pode ser feito diretamente em C, já que não há como garantir que o compilador não irá alterar o conteúdo do SP durante a execução do código. Por isso, é melhor utilizar o assembly para termos a garantia de que o conteúdo de todos os registradores da CPU será preservado fielmente. Além disso, o uso de assembly permite escrever o código mais eficiente possível (limitado apenas à eficiência do programador, é claro!).
A seguir temos o código da função de salvamento de contexto. Ela é declarada como inline pois o objetivo é que o compilador insira todo o código da função no local da sua chamada (evitando assim o uso da pilha, o que seria prejudicial ao funcionamento do nosso agendador).
O código assembly é totalmente auto-explicativo, mas vale a pena fazer alguns comentários sobre o funcionamento do assembly inline no GCC. Para que possamos utilizar variáveis C dentro do assembly é necessário utilizar o chamado extended inline assembly do GCC (tópico 6.44.2 do manual do compilador) cujo formato é o seguinte:
asm [volatile] ( Template : Operandos_saída [ : Operandos_entrada [ : Clobbers ] ])
O template contém o código assembly propriamente dito. Este código pode referenciar registradores da CPU ou endereços de memória. Quando é necessário acessar uma variável C, a mesma deve ser especificada nos operandos de saída (se a mesma for alterada pelo código assembly) ou nos operandos de entrada (opcionais) se a mesma somente for lida. Os Clobbers consistem numa lista de registradores alterados direta ou indiretamente pelo código assembly do template. Cada instrução assembly deve estar escrita dentro de aspas e ser encerrada com \n\t (códigos de barra invertida para linha nova e tab).
Observe que os operandos de entrada e clobbers são opcionais, mas os operandos de saída são obrigatórios. Caso o código não utilize nenhum operando de saída, é possível deixar a lista de operandos de saída em branco (como no nosso caso).
Os operandos de saída e de entrada utilizam ainda uma série de restrições (constraints) que especificam como o operando afeta ou é afetado pelo código. Estas restrições são grafadas dentro de strings e podem assumir valores como:
- “=” – indica que o conteúdo do operando é sobrescrito;
- “+” – indica que o conteúdo do operando tanto é lido como escrito pelo código;
- “m” – indica que o operando é uma posição de memória;
- “r” – indica que o operando é um registrador;
- “i” – indica que o operando é um valor imediato (como o endereço de uma variável).
Note que o símbolo C deve seguir o constraint e estar escrito dentro de parenteses, para maiores detalhes consulte o tópico 6.44.3.1 do manual do GCC.
void inline save_context(void){
asm volatile (
"sel RB3\n\t" // seleciona o banco 3
"mov X,%0\n\t" // X = ulwos_current_task
"clrb A\n\t" // A = 0 (AX = ulwos_current_task)
"shlw AX,1\n\t" // AX = ulwos_current_task *2
"movw BC,%2\n\t" // BC = ulwos_taskSP
// AX = ulwos_taskSP+(current_task*2) => ulwos_taskSP[ulwos_current_task]
"addw AX,BC\n\t"
"movw DE,AX\n\t" // DE = ulwos_taskSP[ulwos_current_task]
"movw AX,SP\n\t" // AX = SP da tarefa
"movw [DE],AX\n\t" // ulwos_task_SP[ulwos_current_task] = SP da tarefa
// agora configuramos o SP para apontar para o topo da pilha de contexto da tarefa
"mov A,#24\n\t" // A = 24 (12 registradores * 2 bytes)
"mov X,%0\n\t" // X = ulwos_current_task
// X = ulwos_current_task+1 (desta forma apontamos o topo da pilha)
"inc X\n\t"
"mulu X\n\t" // AX = 24 * (ulwos_current_task+1)
"addw AX,%1\n\t" // AX = endereço do topo da pilha de contexto
"movw SP,AX\n\t" // o SP agora aponta o topo da pilha de contexto
:
:"m"(ulwos_current_task),"i"(ulwos_task_context),"i"(ulwos_taskSP)
);
// salvamento dos registradores dos bancos 2, 1 e 0 (nesta ordem)
asm ("sel RB2");
save_regs(); // salva os registradores do banco 2
asm ("sel RB1");
save_regs(); // salva os registradores do banco 1
asm ("sel RB0");
save_regs(); // salva os registradores do banco 0
asm ("sel RB3");
}
Note que save_regs() não é uma função, mas uma macro criada para simplificar e melhorar a legibilidade do código, ela é definida como:
#define save_regs() asm ("push AX\n\tpush BC\n\tpush DE\n\tpush HL\n\t")
Após a execução da função save_context, todo o conteúdo dos registradores dos bancos 0, 1 e 2 estará preservado na pilha ulwos_task_context da tarefa e o SP da tarefa estará preservado em ulwos_taskSP. O conteúdo do PSW e do PC já foi preservado automaticamente na pilha da tarefa quando ocorreu a interrupção!
O passo seguinte é selecionar a próxima tarefa a ser executada. O agendador round-robin simplesmente incrementa o indicator de tarefa atual (ulwos_current_task) e verifica se o valor corresponde a uma tarefa válida. Caso negativo, o indicador retorna a zero, reiniciando o ciclo de tarefas.
Após a seleção da nova tarefa a ser executada, é necessário recuperar o contexto da mesma, de forma que a execução possa ser restabelecida do ponto em que estava anteriormente. A função restore_context é encarregada de tal operação. O código assembly é autoexplicativo e dispensa maiores comentários.
void inline restore_context(void){
asm volatile (
// configura o SP para apontar para a pilha de contexto
"mov A,#24\n\t" // A = 24
"mov X,%0\n\t" // X = ulwos_current_task
"mulu X\n\t" // AX = 24 * (ulwos_current_task)
// AX = endereço de ulwos_task_context[ulwos_current_task]
"addw AX,%1\n\t"
"movw SP,AX\n\t" // SP aponta para o topo da pilha de contexto
:
:"m"(ulwos_current_task),"i"(ulwos_task_context)
);
// agora restauramos os bancos 0, 1 e 2
asm ("sel RB0");
restore_regs(); // restaura o banco 0
asm ("sel RB1");
restore_regs(); // restaura o banco 1
asm ("sel RB2");
restore_regs(); // restaura o banco 2
asm volatile (
"sel RB3\n\t"
"mov X,%0\n\t" // X = ulwos_current_task
"clrb A\n\t" // A = 0 (AX = ulwos_current_task)
"shlw AX,1\n\t" // AX = ulwos_current_task*2
"movw BC,%1\n\t" // BC = ulwos_taskSP
"addw AX,BC\n\t" // AX = endereço de ulwos_taskSP+ (ulwos_current_task*2) => ulwos_taskSP[ulwos_current_task]
"movw DE,AX\n\t" // DE = endereço de ulwos_taskSP[ulwos_current_task]
"movw AX,[DE]\n\t" // AX = ulwos_taskSP[ulwos_current_task]
"movw SP,AX\n\t" // configura o SP para a tarefa atual
"reti\n\t" // retorna da interrupção (muda o contexto)
:
:"m"(ulwos_current_task),"i"(ulwos_taskSP)
);
}
Assim como no caso de save_regs(), restore_regs() nada mais é do que uma macro que desempilha os registrados da memória. Sua definição é a seguinte:
#define restore_regs() asm ("pop HL\n\tpop DE\n\tpop BC\n\tpop AX\n\t")
Agora que já temos código capaz de comutar tarefas em execução, é necessário uma função para criar uma nova tarefa. E isso fica para o próximo artigo!








