P I C   -   A P P U N T I    D I    U T I L I Z Z O
 

(avvertenze)

[Precedente]   [Indice principale]

[Multitasking]    [Sistema di debug]   [Adattatore TTL --> EIA "in cable"]

MULTITASKING


Multitasking significa avere in esecuzione su un unico micro piu' programmi contemporaneamente. Ogni programma svolge una particolare funzione di controllo e puo' essere chiamato processo o task. I task possono essere indipendenti tra loro oppure svolgere un compito di gruppo scambiandosi informazioni attraverso celle di memoria. Naturalmente il parallelismo e' solo apparente, in quanto in realta' il micro porta avanti singolarmente i vari programmi un'istruzione per volta, ma se questo meccanismo e' sufficientemente rapido si ottiene l'apparenza (e di fatto l'esistenza) di piu' programmi contemporanei in esecuzione. L'uso del multitasking permette di semplificare enormemente la scrittura di programmi complessi che devono tenere sotto controllo molti processi nello stesso tempo.

Il multitasking puo' essere di tipo preempitive o non preempitive. Nel primo caso i programmi sono scritti nel modo consueto (come se fossero l'unico programma presente nel micro) ed e' compito dello scheduler del sistema operativo avviarli e interromperli a intervalli di tempo prestabiliti, questa soluzione pero' e' molto complessa e sicuramente non realizzabile con un piccolo microcontroller come il PIC 16F84.
 
Il modo non preempitive invece prevede uno scheduler formato da un semplicissimo loop senza fine che richiama ciclicamente tutti i task come come normali subroutines. E' compito di ogni task usare meno tempo possibile e ritornare subito il controllo allo scheduler. Per fare questo i programmi vanno scritti sotto forma di macchine a stati finiti. Le macchine a stati finiti (FSM) sono rappresentabili da diversi stati in cui possono venirsi a trovare, e ogni stato stato rappresenta una condizione di attesa di un qualche evento.

Quando quell'evento si verifica si possono eseguire delle operazioni, ed effettuare la transizione verso un altro stato (che verrà eseguito alla successiva chiamata del task).

Quando un programma viene scritto sotto forma di FSM, non presenta più i caratteristici tempi morti di attesa tipici della forma convenzionale di scrittura, ad esempio per l'attesa della pressione di un pulsante. Ogni attesa è trasformata in uno stato, che controlla rapidamente se l'evento atteso si è verificato e poi ritorna subito il controllo allo scheduler che chiama il task successivo. È evidente che con questo sistema si possono tenere sotto controllo diverse cose nello stesso tempo, con una semplicità altrimenti impossibile.

Una macchina a stati finiti e' composta da una serie di stati in cui puo' permanere finche' non si verifica un evento o una condizione. Quando questa si verifica avviene una transizione ad un altro stato. Dal punto di vista programmativo ogni stato e' una subroutine che viene chiamata solo se corrisponde allo stato attivo del momento. Ogni task deve quindi disporre almeno di una variabile di stato, e quando viene chiamato deve eseguire la specifica routine interna.

    IF STATO=0 THEN
         ROUTINE0
    ELSE IF STATO=1 THEN
         ROUTINE1
    ELSE IF STATO=2 THEN
         ROUTINE2
    ecc...

Il controllo di una macchina a stati finiti si effettua quindi cambiando il valore della variabile di stato all' interno delle singole routines (stati) , in questo modo alla successiva chiamata del task verra' eseguito il nuovo stato. 

In assembler possiamo usare l'istruzione addwf applicata al program counter per costruire l'equivalente di un "on n goto" del BASIC, o un "case" del Pascal, o uno "switch" del C, e un generico scheletro di task puo' essere il seguente:


   ;-------------------------------------------------------------
   task1      movf  stat,w    ;carica in w la variabile stat
              addwf PCL,f     ;somma al program counter
              goto  routine0  ;goto eseguito se stat=0
              goto  routine1  ;goto eseguito se stat=1
              goto  routine2  ;goto eseguito se stat=2

   routine0   .....
              .....
              return

   routine1   .....
              .....
              return

   routine2   .....
              .....
              return
   ;-------------------------------------------------------------

ATTENZIONE: usando l'istruzione addwf PCL (detta anche computed goto cioè salto calcolato) e' semplice creare delle strutture "on n goto", pero'  bisogna stare molto attenti all'impostazione del PCLATH e ai bordi delle pagine di programma da 256 byte ciascuna come gia' visto nel paragrafo sulle lookup tables. In pratica nessuna parte del task deve essere posta ad indirizzi che si trovano a cavallo tra due pagine. E'percio' meglio ricorrere al controllo numerico del valore della variabile di stato come mostrato piu' avanti.
 

Un modo per risolvere il problema delle pagine è quello di impostare correttamente il registro PCLATH, che contiene i bit più significativi dell'indirizzo di memoria programma quando vengono effettuate operazioni che coinvolgono direttamente il registro PCL. Nell'esempio seguente si usano le direttive per il compilatore HIGH e LOW per indicare la parte più alta e più bassa dell'indirizzo della memoria programma a cui iniziano le istruzioni di salto. A questo indirizzo viene sommato lo stato corrente aggiustando il PCLATH se la parte bassa dell'indirizzo supera il valore 255:
 

   ;-------------------------------------------------------------
   task1      movlw HIGH Ttask1  ;carica in w parte alta indirizzo Ttask1
              movwf PCLATH       ;la mette nel PCLATH
              movlw LOW  Ttask1  ;carica in w parte bassa indirizzo Ttask1
              addwf stat,w       ;la somma allo stato
              btfsc STATUS,C     ;se non overflow skip
              incf  PCLATH,F     ;altrimenti incrementa PCLATH
              movwf PCL          ;mette parte bassa indirizzo nel program counter
   Ttask1     goto  routine0     ;goto eseguito se stat=0
              goto  routine1     ;goto eseguito se stat=1
              goto  routine2     ;goto eseguito se stat=2

   routine0   .....
              .....
              return

   routine1   .....
              .....
              return

   routine2   .....
              .....
              return
   ;-------------------------------------------------------------

Prima di iniziare a richiamare i processi e' naturalmente indispensabile inizializzare le loro variabili di stato ed eventualmente impostare dei parametri di partenza se l'applicazione lo richiede, in generale per predisporre un sistema multitasking su un PIC si dovrebbero mettere in sequenza le seguenti operazioni:

    1) IMPOSTAZIONE PORTE DI I/O E ALTRE PERIFERICHE
    2) IMPOSTAZIONE VARIABILI DI STATO E ALTRI PARAMETRI
    3) LOOP SENZA FINE CHE CHIAMA I TASK

L'uso di questo sistema non preempitive permette inoltre di regolare con assoluta precisione il tempo di avanzamento dei task. Infatti, se si sincronizza il loop dello scheduler con il timer interno del PIC, si possono avviare i processi ad intervalli di tempo ben definiti, per esempio ogni 4,096 ms. A questo punto sappiamo che tra una chiamata e l'altra di un task passera' esattamente questo tempo, e questo rende semplice realizzare processi di temporizzazione e controllo durate. I vincoli naturalmente sono da un lato che la somma dei tempi di esecuzione di tutti i task non deve essere mai superiore al ciclo dello scheduler, e dall'altro che i processi non possono gestire segnali o eventi piu' veloci di questo ciclo. Avere processi che avanzano ogni 4,096 ms significa poter realizzare controlli in tempo reale che si aggiornano circa 244 volte al secondo.

Nota: Il motivo per cui ho scritto 4,096 ms e non 4 e' che il timer interno puo' dividere il clock per un numero limitato e fisso di valori,  e 4,096 ms e' uno di quelli ottenibili con un normale quarzo da 4 Mhz.

L'esempio seguente indica come predisporre il timer0 di un PIC16F84 per far avanzare i task di un passo ogni 8,192 ms esatti:
 

   ;------------------------------------------------------------------------------------
   ; DEFINIZIONI
   ;------------------------------------------------------------------------------------
                   PROCESSOR 16F84a  ;clock 4 Mhz
                   RADIX     DEC
                   INCLUDE   "P16F84a.INC"
                   __CONFIG  11111111110001b

   #define         Bank0   bcf STATUS,RP0
   #define         Bank1   bsf STATUS,RP0

                   ORG     0CH

   stat            RES     1       ;variabile di stato per task1

   ;------------------------------------------------------------------------------------
   ; INIZIALIZZAZIONE
   ;------------------------------------------------------------------------------------
                   ORG     0

   ;---------------PREDISPOSIZIONE TIMER

                   Bank1
                   bcf     OPTION_REG,PSA  ;prescaler assegnato a tmr0
                   bcf     OPTION_REG,T0CS ;conteggio da clock interno
                   movlw   11111100b
                   andwf   OPTION_REG,f    ;divisione prescaler per 32

   ;---------------INIZIALIZZAZIONE VARIABILI

                   Bank0
                   clrf    stat           ;Azzera variabile stato per task1

   ;---------------AVVIA CONTEGGIO

                   clrf    TMR0           ;azzera contatore del timer
                   bcf     INTCON,T0IF    ;azzera bit overflow del timer

   ;------------------------------------------------------------------------------------
   ; LOOP PRINCIPALE CHE CHIAMA I PROCESSI OGNI 8,192 ms
   ;------------------------------------------------------------------------------------
   mainloop        btfss   INTCON,T0IF    ;test se overflow timer (viene settato T0IF)
                   goto    mainloop       ;se no attendi
                   bcf     INTCON,T0IF    ;altrimenti riazzera bit timer

                   call    task1          ;e chiama il task 1

                   goto    mainloop       ;poi torna ad attendere il timer

   ;------------------------------------------------------------------------------------ 

Come si diceva prima, per evitare ogni problema di di indirizzamento in cui si puo' incorrere modificando direttamente il program counter (PCL), si può realizzare la struttura del task in un modo un po' piu' complesso, e cioe' facendo dei confronti numerici con il valore della variabile di stato:
 

   ;-------------------------------------------------------------
   task1      movf stat,f
              btfsc STATUS,Z
              goto routine0
              movlw 1
              subwf stat,w
              btfsc STATUS,Z
              goto routine1
              movlw 2
              subwf stat,w
              btfsc STATUS,Z
              goto routine2
              goto routine3

   routine0   ...
              ...
              return

   routine1   ...
              ...
              return

   routine2   ...
              ...
              return

   routine3   ...
              ...
              return
   ;------------------------------------------------------------- 

Usando intensivamente le strutture task, si si puo' anche ricorrere ad una macroistruzione di confronto e salto in  modo da ridurre al minimo le istruzioni da scrivere per realizzare il selettore delle routines, di seguito la macro e il selettore dell'esempio precedente riscritto (che da 12 righe di programma diventa lungo 4!);

   Cpje  macro var,val,addr
         movlw val
         subwf var,w
         btfsc STATUS,Z
         goto  addr
         endm
 

   task1 Cpje stat,0,routine0  ;confronta stat con 0 e jump a routine0 se uguale
         Cpje stat,1,routine1
         Cpje stat,2,routine2
         Cpje stat,3,routine3
         ... 

Come in tutti i sistemi multitasking bisogna fare un po' di attenzione a non creare conflitti tra i processi. Un esempio tipico e' la scrittura su porta di uscita o in memoria. Se due processi vogliono scrivere nello stesso indirizzo la scrittura dell'uno potrebbe annullare quella dell'altro causando effetti indesiderati difficili da scoprire. Una soluzione puo' essere quella di usare un solo "processo scrittore" incaricato di questa funzione che prende i dati inviati dagli altri processi unendoli senza creare conflitti. In generale e' bene che ogni processo "tocchi" solo quello che gli compete, lasciando del tutto inalterato il resto del sistema, come se non fosse mai stato eseguito.
 
 
 

SISTEMA DI DEBUG


Per mettere in pratica tutto quello che si e' detto riguardo a trasmissione seriale e multitasking si puo' creare un task di debug che trasmette ciclicamente 4 byte di dati verso un PC. I 4 valori da trasmettere li possiamo prendere da 4 celle di memoria adibite a celle di debug (chiamate per esempio dbg1 dbg2 dbg3 dbg4). Il protocollo puo' consistere in un byte di allineamento (sincronismo) seguito dai 4 byte dati inviati nel consueto formato start/stop 9600,N,8,2 e da un carattere di checksum che e' la somma a 8 bit di tutti i precedenti. Sul PC un programma apposito deve ricevere questi byte e visualizzarli in formato binario, esadecimale e decimale. A questo punto si ha a disposizione un metodo semplice per visualizzare i valori interni al programma del PIC, infatti basta scrivere nelle celle dbg1..4 i valori per vederli trasferire immediatamente sul video, e questo puo' servire per verificare lo stato delle porte di ingresso, o il risultato di un'operazione aritmetico/logica, o l'evoluzione dello stato di un task al verificarsi degli eventi previsti. Per sperimentare il multitasking possiamo anche far contemporaneamente lampeggiare un led, per esempio collegato sul pin RA0.

Durante il debug deve essere usato un altro pin configurato come uscita, che va collegato ad un adattatore TTL/EIA per essere compatbile con la seriale del PC. Il programma di ricezione su PC contolla continuamente la congruenza dei dati in arrivo, se ci sono errori segnala sulla barra del titolo "NO SYNC OR FRAME ERROR", o se per 200 ms non riceve dati segnala "NO SIGNAL".

[Download visualizzatore picdebug.zip (87K)]
(Scompattarlo e lanciare picdebug.exe, riceve solo da COM1)
 

Il programma del PIC: dbgdemo.asm
Include per pic16f84a: P16F84a.INC
Include powerasm: pwrasm.inc
L'eseguibile: DBGDEMO.HEX
Il programma dimostrativo dbgdemo.asm legge i bit della porta B e li invia come primo byte del campo dati della trama, gli altri 3 byte sono a zero. Prima del campo dati viene trasmesso il carattere di sincronizzazione 170, alla fine dei dati viene trasmesso il checksum (la somma a 8 bit di tutti i precedenti compreso il sincronismo). Viene inviato un byte ogni 4,096 ms, quando sono stati inviati tutti i 6 byte della trama completa c'e' una pausa di circa 41 ms, poi viene inviata un'altra trama. In contemporanea a questa attivita' il led sul pin RA0 lampeggia costantemente alla frequenza di circa 1Hz.
 

ADATTATORE TTL --> EIA "IN CABLE"



Per poter collegare rapidamente il PIC alla seriale del PC, senza montare ogni volta un apposito circuito, ho racchiuso quest'ultimo all'interno del coperchio di un connettore DB25 femmina. Il cavetto che esce dal connettore ha 3 poli (massa, +5V e segnale a livello TTL) e va collegato alla bredboard su cui si sta provando il PIC. In questo modo ci si puo' dimenticare del problema dell'adattamento e si possono inviare i dati al PC usando una normale uscita TTL (anche open collector). Lo schema e' il seguente:
 


 
 

[Precedente]   [Indice principale]



Pagina creata da Claudio Fin nel 2001  -  Ultimo aggiornamento pagina 4-11-2005