Pagine

lunedì 21 maggio 2012

Dalle Qt alla DEMOJM, ovvero come spedire "velocemente" i nostri bit dal PC all'esterno: il firmware su DEMOJM. (parte seconda)



Carissimi lettori di HWdebug siamo alla seconda puntata della serie di articoli intitolata "Dalle Qt alla DEMOJM, ovvero come spedire velocemente i nostri bit dal PC all'esterno" dedicata a tutti colori che necessitano di creare un sistema di comunicazione "veloce" fra il PC e un hardware esterno. Come abbiamo spiegato nel primo articolo, che trovate qui, il nostro scopo non è quello di creare un'interfaccia di comunicazione super veloce o super affidabile, ma qualcosa che nel giro di una giornata possa funzionare correttamente. In questa puntata parleremo di come programmare il firmware della DEMOJM per colloquiare via seriale col PC o con altre periferiche seriali.

Il microcontrollore MCF51JM128 fornisce due moduli per la comunicazione seriale indipendenti, denominati SCI, Serial Communication Interface, o, a volte, anche UARTs, Universal Asynchronous receiver/transmitters. Tali moduli possono essere utilizzati per comunicare con il PC tramite RS232 o per realizzare la comunicazione con altri microcontrollori.


La DEMOJM è dotata di un'interfaccia di programmazione denominata PEMICRO embedded multilink che mette a disposizione un'interfaccia seriale virtuale da poter utilizzare per colloquiare con il microcontrollore. Tale interfaccia è collegabile all'interfaccia SCI 1 del micro MCF51JM128 tramite due jumpers presenti sulla scheda.

Purtroppo però abbiamo trovato delle discrepanze fra la denominazione dei jumper sullo schematico della DEMOJM, giunto alla revisione D e messo a disposizione dalla Freescale qui, e la serigrafia della scheda, anch'essa giunta alla revisione D. In particolare nello schematico si trovano alla pagina 3 del pdf e vengono denominati J5. Nella realtà i due jumper, in accordo con la serigrafia, si trovano accanto al connettore USB di programmazione e sono serigrafati come J4. In ogni caso affinché il PEMICRO embedded multilink sia connesso alla SCI 1 c'è bisogno che i due jumper siano installati nella direzione di inserimento del connettore USB. L'immagine seguente mostra i due jumper connessi correttamente.



La trasmissione seriale che andiamo ad inizializzare avrà le seguenti caratteristiche:
  • Velocità: 115200 baud;
  • Lunghezza carattere: 8 bit;
  • Bit di parità: nessuno;
  • Controllo di flusso: OFF.
Lo sviluppo del firmware per il microcontrollore è avvenuto all'interno dell'ambiente di sviluppo Codewarrior 10.2. Per non dilungarci troppo ma anche per non uscire fuori tema non ci soffermiamo sull'inizializzazione del microcontrollore e del clock di sistema che può essere realizzata utilizzando il plugin Processor Expert.

Per implementare le funzioni necessarie alla comunicazione seriale abbiamo preferito creare una piccola libreria. Allo scopo sono stati aggiunti al progetto due file denominati rispettivamente "sci.h" e "sci.c" che raccolgono tutte le funzioni necessarie al funzionamento della SCI. Il primo è stato creato nella sezione degli header files del progetto (cartella "Project_Headers") mentre il secondo nella sezione dei sorgenti del progetto (cartella "Sources"). Ricordiamo che affinché il tutto funzioni in file "sci.h"dovrà essere incluso con la direttiva del C  
#include "sci.h"  all'interno del file main.c. Altrimenti il compilatore non riuscirà a vedere la dichiarazione delle nostre funzioni e darà errore in fase di compilazione.

All'interno del file sci.h troviamo le seguenti righe di codice.
#ifndef _SCI_H_
#define _SCI_H_

#include "mcf51jm128.h"

/* Funzioni di inizializzazione */

void sci_init();
void sci_stop();

/* Funzioni di servizio */

int get_string(char * string, unsigned int length);
unsigned char sci_poll (void);
void sci_send (const char *text);
#endif 
Possiamo notare all'interno del file le dichiarazioni delle funzioni che utilizziamo e l'inclusione del file di header mcf51jm128.h che contiene le definizioni dei registri del microcontrollore MCF51JM128. In tal modo potremmo indirizzare i registri utilizzando le loro definizioni mnemoniche e non con l'indirizzo assoluto di memoria corrispondente.

Le funzioni di inizializzazione sci_init() e sci_stop() servono ad inizializzare e fermare l'interfaccia SCI1 del microcontrollore. La funzione get_string(char *string, unsigned int length) permette di estrarre dal buffer di ricezione una stringa di lunghezza length e inserirla nel vettore puntato da string. La funzione serial_poll() permette invece di ritornare il numero di caratteri presenti nel buffer di ricezione. La funzione serial_send(const char *text) permette invece di inviare la stringa puntata da text al PC.

Di seguito troviamo le righe del file sci.c che contiene l'implementazione delle funzioni viste in precedenza. Per gestire la comunicazione si utilizzano due buffer circolari: uno in trasmissione e l'altro in ricezione. La lunghezza di questi buffer viene determinata dalla costante SCIRX_BUFFER_MASK ed è fissata attualmente a 256 caratteri che è anche la lunghezza massima della stringa ricevibile/trasmissibile a buffer vuoti.

Le variabili rx_head, rx_tail ed tx_head, tx_tail permettono di gestire il meccanismo della coda circolare. In sostanza head costituisce il puntatore alla testa del buffer circolare mentre tail è il puntatore alla coda. Inizialmente questi buffer puntano alla stessa locazione, ad esempio alla locazione 0. Ogni volta che viene inserito un carattere nel buffer questo viene inserito nella locazione indicata dal puntatore di coda e quest'ultimo viene incrementato per puntare alla locazione successiva. Quando si estrae invece un carattere dal buffer si estrae quello indicato dal puntatore di testa e questo viene incrementato di una unità. In entrambe i casi, dopo aver incrementato il puntatore, si fa l'AND bit a bit con la variabile SCIRX_BUFFER_MASK per gestire il meccanismo di overflow del buffer e tornare all'inizio. Per l'implementazione del buffer circolare è doveroso ringraziare il mio amico/collega Giorgio B. per avermi fornito il codice da cui prendere spunto.

#include "sci.h"

#define SCIRX_BUFFER_MASK 0xFF
static volatile char rx_head, rx_tail;
static volatile char tx_head, tx_tail;
volatile char tx_buffer[SCIRX_BUFFER_MASK + 1];
volatile char rx_buffer[SCIRX_BUFFER_MASK + 1];

void sci_init()
SCI1BDH = 0x00;
SCI1BDL = 0x0D;

SCI1C1  = 0x00;
SCI1C2  = 0x2C;
SCI1C3  = 0x20;

tx_head = 0;
tx_tail = 0;
rx_head = 0;
rx_tail = 0;
}

void sci_stop()
{
SCI1C2  = 0x00;
SCI1BDH = 0x00;
SCI1BDL = 0x00;    

tx_head = 0;
tx_tail = 0;
}

void sci_send (const char *text)
{
    if (text) 
        while (*text) 
        {
            tx_buffer[tx_tail++] = *text++;
        }
    SCI1C2_TIE = 1;
}


unsigned char sci_poll (void)
{
    return (unsigned char) ((rx_tail - rx_head) & SCIRX_BUFFER_MASK);
}

int get_string(char * string, unsigned int length)
{
    int i;
    if( ((rx_tail - rx_head) & SCIRX_BUFFER_MASK) >= length )
    {
        for(i=0;i<length;i++)
        {
            *(string + i) = rx_buffer[rx_head++];
            rx_head &= SCIRX_BUFFER_MASK;
        }
        
        return 1;
    }
    else
    {
        return -1;
    }
}

/* Interrupt service routines: */
interrupt VectorNumber_Vsci1tx void isrVsci1tx (void)
{
    if (tx_tail != tx_head)
    {
        (void) SCI1S1;
        SCI1D = (unsigned char) tx_buffer[tx_head++];
    } 
    else
    {
        SCI1C2_TIE = 0;
    }
}

interrupt VectorNumber_Vsci1rx void isrVsci1rx (void)
{
    if (SCI1S1_RDRF && (SCI1D != 0)) 
    {
        rx_buffer[rx_tail++] = (char) SCI1D;
        rx_tail &= SCIRX_BUFFER_MASK;
    }
}
Proviamo ora a commentare brevemente tutte le funzioni implementate per cercare di capire cosa fanno. La prima e più importante è la funzione sci_init() il cui codice ha il compito di inizializzare l'interfaccia SCI per la comunicazione seriale. La descrizione dettagliata dei registri coinvolti in questa configurazione si trova nel Capitolo 12 del manuale di riferimento dell'MCF51JM128 che si può scaricare da qui.

void sci_init()
{
    SCI1BDH = 0x00;
    SCI1BDL = 0x0D;
    
    SCI1C1  = 0x00;
    SCI1C2  = 0x2C;
    SCI1C3  = 0x20;
    
    tx_head = 0;
    tx_tail = 0;
    rx_head = 0;
    rx_tail = 0;
}
void sci_stop()
{
    SCI1C2  = 0x00;
    SCI1BDH = 0x00;
    SCI1BDL = 0x00;    
    
    tx_head = 0;
    tx_tail = 0;
}


La funzione sci_init() inizia con la scrittura del registro di configurazione a 16 bit denominato SCI1BD. Tale registro è costituito da due registri a 8 bit denominati rispettivamente SCI1BDH e SCI1BDL. Nel primo dei due il bit più significativo (MSb per brevità) se viene messo a 1 attiva l'interrupt alla determinazione del segnale di break detect del bus LIN, nel nostro caso non interessa e viene messo a 0. Il bit 6 attiva invece l'interrupt all'input RxD e anche questo in tal caso viene posto a zero. Il bit 5 viene posto sempre a zero e i bit a seguire dal 4 fino al bit 0 il meno significativo (LSb), costituiscono invece i bit più significativi del modulo BR del divisore che permette di impostare la velocità della porta. Infatti questi 5 bit, insieme a tutti quelli del registro SCI1BDL determinano il modulo BR del divisore del bus clock che determina il baud rate della porta.

Per fissare il baud rate della porta bisogna impostare il modulo BR del divisore secondo la seguente equazione
BR = BUSCLK/(16*baud rate)
nel nostro caso si era impostato il microcontrollore per avere un bus clock di 24 MHz, e volevamo avere un baud rate di 115200 baud da cui si ottiene che BR dovrà valere 13 che, su 8 bit in esadecimale corrisponde a 0x0D, che è appunto il valore assegnato al modulo del divisore. Si sottolinea che la sequenza con cui deve essere fatta la scrittura dei registri SCI1BDH e SCI1BDL non è casuale, in quanto la scrittura del valore nel registro SCI1BDH avviene al momento della scrittura del registro SCI1BDL e quindi necessariamente la seconda deve seguire la prima.

Il registro SCI1C1 controlla invece i principali parametri della comunicazione seriale e per ottenere le specifiche riportate sopra basta metterlo tutto a 0. Il registro SCI1C2 è un registro di abilitazione degli interrupt e delle periferiche della SCI. Viene impostato così per far si che la SCI1 generi un interrupt alla ricezione di un carattere lungo il canale e per abilitare il trasmettitore e ricevitore. In tal modo gestiremo la comunicazione con un meccanismo ad interruzioni che permette di lasciare libera la CPU di eseguire altri compiti e la interrompe quando c'è da trasmettere o quando c'è da ricevere un carattere.  Il registro SCI1C3 contiene invece l'impostazione che il pin TxD sia un output nella modalità single wire.

Alla fine della funzione vengono inizializzate a 0 tutte le variabili che permettono il funzionamento dei buffer circolari in trasmissione e ricezione. Nella funzione sci_stop() vengono in sostanza annullate le configurazioni fatte in precedente ma in particolare vengono posti a 0 i bit 2 e 3 del registro SCI1C2 che attivano ricevitore e trasmettitore.

La funzione sci_send (const char *text) permette di caricare nel buffer circolare di trasmissione (verso il PC per intenderci) una stringa di caratteri da inviare al computer.
void sci_send (const char *text)
{
    if (text) 
        while (*text) 
        {
            tx_buffer[tx_tail++] = *text++;
        }
    SCI1C2_TIE = 1;
}
Per inviare i dati si controlla l'esistenza del puntatore al testo e fino a quando il contenuto del puntatore è diverso da zero la stringa viene ricopiata nel buffer di trasmissione. L'istruzione SCI1C2_TIE = 1; serve ad attivare l'interrupt della SCI1 allo svuotamento del buffer di trasmissione. In tal modo quando viene inviato un carattere e si svuota il buffer di trasmissione verrà generato un interrupt nella cui routine di servizio si caricherà (se ce n'è sono) un altro carattere e partirà la trasmissione di nuovo la trasmissione. Questa funzione può essere chiamata dal main per inviare una stringa di dati al PC.

La funzione sci_poll(void) permette di ritornare invece il numero di caratteri presenti nel buffer circolare di ricezione.
unsigned char sci_poll (void)
{
    return (unsigned char) ((rx_tail - rx_head) & SCIRX_BUFFER_MASK);
}
Semplicemente si ritorna la differenza fra il puntatore alla coda e alla testa del buffer circolare in AND con la costante SCIRX_BUFFER_MASK. Tale AND bit a bit permette di tenere in conto la circolarità del buffer.

La funzione get_string(char * string, unsigned int length) permette invece di estrarre una stringa di lunghezza length dal buffer circolare e inserirla nel vettore string.

int get_string(char * string, unsigned int length)
{
    int i;
    if( ((rx_tail - rx_head) & SCIRX_BUFFER_MASK) >= length )
    {
        for(i=0;i<length;i++)
        {
            *(string + i) = rx_buffer[rx_head++];
            rx_head &= SCIRX_BUFFER_MASK;
        }
        
        return 1;
    }
    else
    {
        return -1;
    }
}
All'inizio del corpo della funzione si controlla che il numero di byte presente nel buffer di ricezione sia maggiore di quello che si vuole leggere altrimenti si ritorna errore (-1). Successivamente si utilizza il ciclo for per fare la copia dei caratteri ricevuti nella stringa di uscita. Si nota bene il meccanismo che incrementa il puntatore alla testa del buffer circolare all'estrazione di ogni carattere. Si può utilizzare tale funzione all'interno del main in una chiama stile
n = sci_poll();
if(n)
{ 
       get_string(stringa, n);
} 
dove n è un unsigned char e stringa è un vettore di char di lunghezza maggiore di n. Dopo questa chiamata se tutto va a buon fine in stringa ci sono i dati ricevuti dal PC.

Alla base del funzionamento della nostra libreria per il funzionamento della SCI1 sono le routine di servizio alle interruzioni riportate in seguito. La direttiva interrupt prima del simbolo VectorNumber_Vsci1(t o r)x permette al compilatore del Codewarrior di capire che si tratta della definizione di una Interrupt Service Routine il cui numero nella tabella dei vettori delle interruzioni è indicato dal simbolo che lo segue definito nell'header file mcf51jm128.h.

interrupt VectorNumber_Vsci1tx void isrVsci1tx (void)
{
    if (tx_tail != tx_head)
    {
        (void) SCI1S1;
        SCI1D = (unsigned char) tx_buffer[tx_head++];
    } 
    else
    {
        SCI1C2_TIE = 0;
    }
}
interrupt VectorNumber_Vsci1rx void isrVsci1rx (void)
{
    if (SCI1S1_RDRF && (SCI1D != 0))
    {
        rx_buffer[rx_tail++] = (char) SCI1D;
        rx_tail &= SCIRX_BUFFER_MASK;
    }
}
La routine isrVsci1tx(void) serve l'interrupt generato alla trasmissione di un carattere lungo la seriale. Quando la SCI1 finisce di trasmettere un carattere genera questo interrupt in cui si controlla la presenza di ulteriori caratteri nel buffer di trasmissione. Se questi ci sono vengono caricati nel registro di trasmissione dalla sequenza di istruzioni
(void) SCI1S1;

SCI1D = (unsigned char) tx_buffer[tx_head++];
che a sua volta provvederà alla trasmissione di un altro carattere che, terminata, genererà un altro interrupt dello stesso tipo.

La routine successiva, isrVsci1rx invece si occupa invece di gestire la ricezione dei caratteri dalla seriale. In essa in pratica si controlla che sia stato ricevuto un carattere (diverso dal carattere 0) e lo si memorizza nel buffer circolare di ricezione incrementando il puntatore alla coda del buffer.

Bene, dopo aver descritto le routine della libreria e il loro semplice utilizzo all'interno del main, termina questo lungo articolo sperando di non avervi annoiato. Per qualsiasi dubbio o chiarimento si potrà inserire un commento in calce all'articolo e cercheremo di risolverlo insieme. In tal modo i dubbi di ognuno serviranno a chiarire eventuali incertezze e dubbi degli altri.

Rimandiamo l'appuntamento alla prossima puntata dove vedremo come realizzare la comunicazione dal lato PC.

Ciao a tutti!!!







Nessun commento:

Posta un commento