ULWOS – Multitarefa no RL78: Comutador de tarefas

ULWOS Multitarefa Multitarefa no RL78
Este post faz parte da série ULWOS - Multitarefa no RL78

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!

ULWOS - Multitarefa no RL78

ULWOS – Multitarefa no RL78: Introdução ULWOS – Multitarefa no RL78: Criando tarefas
Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Notificações
Notificar
0 Comentários
recentes
antigos mais votados
Inline Feedbacks
View all comments
Home » Software » Firmware » ULWOS – Multitarefa no RL78: Comutador de tarefas

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: