Multitask Com Time-Slicing no PIC18F

Ao contrário dos microcontroladores da família PIC16F, os microcontroladores PIC18F permitem operações de leitura e escrita no Stack de hardware bem como leitura e escrita de endereços no Program Counter pelo programa em execução, características essas que criam oportunidades para explorar a construção de programas mais sofisticados como seria o caso de um sistema multitarefas rodando num microcontrolador.

O Program Counter

O Program Counter é formado pelo trio de registradores PCU:PCH:PCL. Os registradores PCU:PCH não podem ser lidos ou escritos diretamente pelo programa. Apenas o registrador PCL pode ser lido ou estrito diretamente. Os registradores PCU:PCH podem ser lidos ou escritos indiretamente através do par de registradores PCLATU:PCLATH quando uma instrução de leitura ou escrita é realizada no registrador PCL como   MOVF PCL, w   ou   ADDWF PCL, f.

O Stack de Hardware

O stack de hardware dos microcontroladores PIC18F possuem 32 entradas para armazenamento de dados ou endereços de retorno de insturções CALL, RCALL ou interrupções. O stack é controlado pelo registrador especial STKPTR que sempre aponta para o topo do Stack.

Quando uma instrução CALL, RCALL é executada ou uma interrupção ocorre, o registrador STKPTR é incrementado, apontando para uma nova entrada no stack e o endereço da instrução seguinte ao CALL, RCALL ou o interrupção é armazenado nessa nova entrada. Além disso, o mesmo conteúdo (endereço) é armazenado nos registradores TOSU:TOSH:TOSL que podem ser lidos e escritos pelo programa. Carregando os regitradores TOSU:TOSH:TOSL com endereços diferentes, é possível fazer desvios para pontos diferentes do program ao invés de retornar sempre para o endereço da instrução seguinte ao CALL, RCALL ou da interrupção.

O uso das instruções RETURN, RETFIE e RETLW retira do Stack o conteúdo apontado pelo STKPTR e decrementa esse registrador, fazendo-o apontar para a entrada anterior. O conteúdo dos registradores TOSU:TOSH:TOSL é colocado no Program Counter fazendo a CPU desviar para a instrução seguinte ao CALL, RCALL ou interrupção. Se os registradores TOSU:TOSH:TOSL forem carregados com um endereço diferente, a CPU será direcionada a extrair as próximas instruções a partir desse endereço.

As instruções PUSH e POP também podem ser usadas para armazerna e retirar valores do topo do Stack respectivamente.

O que podemos concluir das informações acima

  1. Toda leitura do registrador PCL carregará os registradores PCLATU e PCLATH com os conteúdos dos registradores PCU e PCH do Program Counter (PC).

  2. Toda escrita no registrador PCL carregará os registradores PCU e PCH com os conteúdos dos registradores PCLATU e PCLATH causando um desvido do fluxo do programa para o endereço representado pelo novo conteúdo desses registradores.

  3. O topo do Stack pode ser lido e gravado através dos registradores TOSU:TOSH:TOSL. Os registradores TOSU:TOSH:TOSL podem ser usados para forçar um retorno para um endereço desejado quando dentro de uma subrotina chamada pelas instruções CALL, RCALL ou uma interrupção.

O Conceito de Time-Slicing

Time-slicing, ou fatia de tempo, é a denominação de uma técnica de programação, geralmente utilizada em sistema operacionais, para compartilhar os recursos da máquina entre vários programas que "parecem" executar todos ao mesmo tempo. Essa técnica é usada em sistemas que possuem um único processador que deve executar várias tarefas concomitantes.

Num sistema desse tipo, um cronômetro provê um tempo determinado para cada tarefa a ser executada e, quando o tempo de uma tarefa expira, ela é interrompida e seu contexto é salvo numa fila de espera. Outra tarefa é selecionada da fila e, novamente, um tempo é determinado para sua execução. O ciclo se repete para quantas tarefas hajam na fila.

Encadeamento de tarefas

Para organizar e controlar a execução de tarefas independentes, é necessário que uma estrutura de dados contenha a descrição de cada tarefa. Essa estrutura poderia ser uma lista ou fila, onde cada tarefa estaria representada por uma entrada e teria, por exemplo, o endereço de memória do início do código da tarefa, o endereço de interrupção da tarefa e outros campos indicadores do estado da mesma como no exemplo ao lado.

Um programa gerenciador da fila seria o responsável por administrar a ordem da sequência de despacho de cada tarefa. Esse gerenciador também seria o responsável por iniciar o cronômetro antes de despachar cada tarefa.

O cronômetro da tarefa teria a função de interromper a tarefa e armazenar o endereço da interrupção no campo reservado para isso na sua entrada da fila para que, próxima vez em que essa tarefa for despachada, ela inicie no ponto onde foi interrompida.



Implementando Um Programa Multitarefas

Usando os poucos conceitos acima, podemos pensar em desenvolver um programa que pode executar tarefas de maneira controlada por um temporizador (Time-Slicing), isto é, onde cada tarefa será executada durante um intervalo de tempo após o qual será interrompida e outra tarefa será executada.

Para conseguirmos o comportamento esperado, devemos estabelecer o seguinte mecanismo:

  1. Criação de uma fila (QUEUE) de tarefas.
  2. Inicialização da fila de tarefas.
  3. Adição das tarefas na fila.
  4. Administração da fila de tarefas.
  5. Rotina de interrupção de tempo (Time-Slicing).

Criação de uma fila (QUEUE) de tarefas

Supondo-se que cada entrada da fila tenha 7 bytes e que teremos 3 tarefas para executar, a fila pode ser construida na memória RAM de um PIC18F4520 usando-se as seguintes diretivas de linguagem assembly:

taskwrk0 RES 1 ; registrador de trabalho taskptrh RES 1 ; MSB do apontador da tarefa taskptrl RES 1 ; LSB do apontador da tarefa taskqueue RES .28 ; 3 entradas + 1 de controle

Notar que 3 entradas de 7 bytes ocupariam 21 bytes mas criamos uma entrada a mais para usar a última entrada como controle de fim da fila.

Inicialização da fila de tarefas

A inicialização da fila de tarefas consiste em gravar 0xFF em todos os bytes da mesma.

taskinit lfsr FSR0, taskqueue ; carrega indexador FSR0 com endereco da fila movlw .28 ; numero de bytes da fila movwf taskwrk0 ; carrega contador de bytes movlw 0xFF ; W=0xFF taskinit01 movwf POSTINC0 ; move 0xFF para um byte da fila decfsz taskwrk0, f ; fim? goto taskinit01 ; NAO --> proximo byte

Adição das tarefas na fila

Considerando que cada entrada de 7 bytes na fila representa uma tarefa, podemos estruturar cada entrada com campos que tenham os seguintes deslocamentos:

taskaddu EQU 0 ; endereco inicial da tarefa (UPPER) taskaddh EQU 1 ; endereco inicial da tarefa (HIGH) taskaddl EQU 2 ; endereco inicial da tarefa (LOW) taskrstu EQU 3 ; endereco de reinicio da tarefa (UPPER) taskrsth EQU 4 ; endereco de reinicio da tarefa (HIGH) taskrstl EQU 5 ; endereco de reinicio da tarefa (LOW) taskecb EQU 6 ; controle de eventos

Onde as localizações taskaddu:taskaddh:taskaddl contem o endereço do início do código da tarefa.

As localizações taskrstu:taskrsth:taskrstl contem, inicialmente, o endereço do início do código da tarefa e, após a interrupção, conterá o endereço onde a terefa foi interrompida pelo cronômetro.

A localização taskecb é um byte que poderá conter informações de controle para a execução da tarefa.

Como definido no início, criaremos 3 tarefas como a seguir:

Primeira tarefa

A primeira tarefa pisca um LED conectado ao PORTB<1>

ORG 0x100 ; posiciona no endereço 0x100 da memória piscaled btg PORTB, 1 ; inverte estado do LED nop ; delay nop ; delay nop ; delay goto piscaled ; continua...

Segunda tarefa

A segunda tarefa lê um botão conectado ao PORTB<2> e soa um alarme Buzzer conectado ao PORTB<3>.

ORG 0x200 ; posiciona no endereço 0x200 da memória lebotao bsf TRISB, 2 ; PORTB<2> entrada bcf TRISB, 3 ; PORTB<3> saida lebotao1 btfsc PORTB, 2 ; botao pressionado? goto lebotao1 ; NAO --> le o botao bsf PORTB, 3 ; SIM --> dispara o buzzer lebotao2 btfss PORTB, 2 ; botao pressionado? goto lebotao2 ; SIM --> espera liberar o botao bcf PORTB, 3 ; NAO --> desliga o buzzer goto lebotao1 ; continua...

Terceira tarefa

A terceira tarefa incrementa um contador e mostra em LEDs conectados ao PORTC.

ORG 0x300 ; posiciona no endereço 0x300 da memória inccount clrf TRISC ; PORTC saida inccount1 incf contador, f ; soma 1 movf contador, w ; pega o contador movwf PORTC ; mostra no PORTC nop ; delay nop ; delay goto inccount1 ; continua...

Preenchimento da fila com as tarefas

lfsr FSR0, taskqueue ; endereco inicial da fila ; Carrega endereco do inicio de piscaled em taskaddu:taskaddh:taskaddl movlw UPPER(piscaled) ; endereco UPPER da tarefa piscaled movwf POSTINC0 ; salva na fila movlw HIGH(piscaled) ; endereco HIGH da tarefa piscaled movwf POSTINC0 ; salva na fila movlw LOW(piscaled) ; endereco LOW da tarefa piscaled movwf POSTINC0 ; salva na fila ; Carrega endereco de restart de piscaled em taskrstu:taskrsth:taskrstl movlw UPPER(piscaled) ; endereco UPPER da tarefa piscaled movwf POSTINC0 ; salva na fila movlw HIGH(piscaled) ; endereco HIGH da tarefa piscaled movwf POSTINC0 ; salva na fila movlw LOW(piscaled) ; endereco LOW da tarefa piscaled movwf POSTINC0 ; salva na fila clrf POSTINC0 ; zera taskecb ; Carrega endereco do inicio de lebotao em taskaddu:taskaddh:taskaddl movlw UPPER(lebotao) ; endereco UPPER da tarefa lebotao movwf POSTINC0 ; salva na fila movlw HIGH(lebotao) ; endereco HIGH da tarefa lebotao movwf POSTINC0 ; salva na fila movlw LOW(lebotao) ; endereco LOW da tarefa lebotao movwf POSTINC0 ; salva na fila ; Carrega endereco de restart de lebotao em taskrstu:taskrsth:taskrstl movlw UPPER(lebotao) ; endereco UPPER da tarefa lebotao movwf POSTINC0 ; salva na fila movlw HIGH(lebotao) ; endereco HIGH da tarefa lebotao movwf POSTINC0 ; salva na fila movlw LOW(lebotao) ; endereco LOW da tarefa lebotao movwf POSTINC0 ; salva na fila clrf POSTINC0 ; zera taskecb ; Carrega endereco do inicio de inccount em taskaddu:taskaddh:taskaddl movlw UPPER(inccount) ; endereco UPPER da tarefa inccount movwf POSTINC0 ; salva na fila movlw HIGH(inccount) ; endereco HIGH da tarefa inccount movwf POSTINC0 ; salva na fila movlw LOW(inccount) ; endereco LOW da tarefa inccount movwf POSTINC0 ; salva na fila ; Carrega endereco de restart de inccount em taskrstu:taskrsth:taskrstl movlw UPPER(inccount) ; endereco UPPER da tarefa inccount movwf POSTINC0 ; salva na fila movlw HIGH(inccount) ; endereco HIGH da tarefa inccount movwf POSTINC0 ; salva na fila movlw LOW(inccount) ; endereco LOW da tarefa inccount movwf POSTINC0 ; salva na fila clrf POSTINC0 ; zera taskecb

Após executar o código acima, a fila de terefas estará com o seguinte conteúdo:

 Endereço inicial   Endereço interrupção   ECB 
 TASKADDU   TASKADDH   TASKADDL   TASKRSTU   TASKRSTH   TASKRSTL 
00 01 00 00 01 00 00
00 02 00 00 02 00 00
00 03 00 00 03 00 00

Fila de terefas preenchida

Administração da fila de tarefas

A função do administrador da fila de tarefas será selecionar uma das tarefas e passar o controle para ela.

Para sequenciar essa operação, vamos usar o registrador taskptrh:taskptrl já definido anteriormente como um apontador para a tarefa atual.

O administrador extrairá dos campos TASKRSTU:TASKRSTH:TASKRSTL da entrada atual, apontada por taskptrh:taskptrl, o endereço de onde a tarefa deve iniciar e colocará esse endereço no Program Counter para provocar o desvio para a tarefa requerida.

Deve-se notar que todas as tarefas permanecem num loop contínuo.

; Faz FSR0 apontar para a entrada atual da fila lfsr FSR0, taskqueue ; endereco do inicio da file movff FSR0H, taskptrh ; salva HIGH da primeira entrada movff FSR0L, taskptrl ; salva LOW da primeira entrada ; ; Este sera o ponto de retorno da interrupcao de tempo. ; A interrupcao de tempo atualizou o ponteiro de tarefa 'taskptrh:taskptrl' ; com o endereco da proxima entrada da fila de tarefas. ; Este administrado usara o ponteiro para selecionar e despachar a ; proxima tarefa. ; queueAdmin movff taskptrh, FSR0H ; carrega HIGH da entrada atual movff taskptrl, FSR0L ; carrega LOW da entrada atual ; Aponta para o endereço de reinicio da tarefa movlw taskrstu ; deslocamento do ponto de inicio da tarefa addwf FSR0L, f ; soma em LOW btfsc STATUS, C ; vai 1 ? incf FSR0H, f ; SIM --> incrementa o HIGH ; ; Configura o timer antes de passar controle para a tarefa. ; Sera usado um prescaler 1:4 que produzira um tempo de aproximadamente ; 60ms para cada tarefa. ; movlw b'00000001' ; Prescaler 1:4 movwf T0CON ; configura com o prescaler clrf TMR0H clrf TMR0L bsf INTCON, TMR0IE bcf INTCON, TMR0IF bsf INTCON, PEIE bsf INTCON, GIE bsf T0CON, TMR0ON ; ; Carrega o endereco no Program Counter para executar a tarefa. ; ; Nota: ; 1 - A instrucao MOVFF nao pode ser usada para carregar o PCL. ; movff POSTINC0, PCLATU ; carrega UPPER do Program Counter movff POSTINC0, PCLATH ; carrega HIGH do Program Counter movf POSTINC0, w ; endereco LOW movwf PCL ; carrega PCL provocando o desvio para a tarefa

Rotina de interrupção de tempo (Time-Slicing)

A rotina de interrupção do timer nunca retornará para a tarefa que foi interrompida. O retorno será realizado para o administrador da fila que vai despachar a próxima tarefa apontada pelo par taskptrh:taskptrl que será atualizado nesta interrupção para apontar a próxima tarefa.

O endereço de retorno ao administrador será colocado nos registradores TOSU:TOSH:TOSL que serão transferidos ao Program Counter quando a instrução RETFIE for executada. A instrução RETFIE usará a opção FAST para restaurar o contexto (W e STATUS) antes da interrupção ocorrer.

timeSlice movff taskptrh, FSR0H ; HIGH do endereco da entrada da tarefa atual movff taskptrl, FSR0L ; LOW do endereco da entrada da tarefa atual movlw taskrstu ; deslocamento o endereco de reinicio addwf FSR0L, f ; btfsc STATUS, C ; incf FSR0H, f ; movf TOSU, w ; UPPER do endereco onde parou movwf POSTINC0 ; salva UPPER do endereco onde parou movf TOSH, w ; HIGH do endereco onde parou movwf POSTINC0 ; salva HIGH do endereco onde parou movf TOSL, w ; LOW do endereco onde parou movwf POSTINC0 ; salva LOW do endereco onde parou movf POSTINC0, w ; avanca para a ECB apontando para a proxima entrada movff FSR0H, taskptrh ; salva HIGH da proxima entrada movff FSR0L, taskptrl ; salva LOW da proxima entrada btfss INDF0, 7 ; ultima entrada ? goto timeSlice1 ; NAO --> retorna ao administrador lfsr FSR0, taskqueue ; SIM --> aponta inicio da fila movff FSR0H, taskptrh ; salva HIGH no inicio da fila movff FSR0L, taskptrl ; salva LOW no inicio da fila timeSlice1 movlw UPPER(queueAdmin) ; UPPER do retorno ao administrador movwf TOSU ; carrega TOSU movlw HIGH(queueAdmin) ; HIGH do retorno ao administrador movwf TOSH ; carrega TOSH movlw LOW(queueAdmin) ; LOW do retorno ao administrador movwf TOSL ; carrega TOSL bcf INTCON, TMR0IF ; limpa flag de interrupcao do timer bsf INTCON, TMR0IE ; habilita interrupcao do timer retfie FAST ; retorna ao adimistrador recuperando ; o contexto



O programa completo

Copie o programa completo que se encontra na área de texto abaixo.

Esse programa foi compilado e testado numa placa de desenvolvimento antes de ser publicado.

Se desejar, pode baixar o arquivo HEX para gravá-lo diretamente num microcontrolador PIC18F4520.













H P S P I N

Desde 04 de Março de 2010

Atualização: 22 de Nov de 2025