Cos'è ElectroYou | Login Iscriviti

ElectroYou - la comunità dei professionisti del mondo elettrico

18
voti

PIC18F47J53 nozioni di base sulla seriale

Potere interagire con un microcontrollore attraverso una linea seriale è una cosa semplice e potente. Può sembrare una periferica di "serie B" ma l' interfaccia seriale è un qualcosa che in tempi passati ha dato grandi possibilità. Oggi la si usa più che altro, per connettersi con moduli GPS o GSM, oppure per semplici comunicazioni fra microcontrollori o PC ma in passato era l' interfaccia per definizione. Vale la pena di analizzarla semplicemente per quella che è, e vedere come e perché utilizzarla con i microcontrollori in generale ed il PIC18F47J53 in particolare. Per la sperimentazione pratica utilizzerò la scheda PIERIN PIC18.

Indice

La linea seriale

Una brevissima descrizione di cos'è una linea seriale, breve perché in rete di materiale tecnico e storico sulle linee seriali se ne trova in quantità.
In passato era normale avere un unico computer chiamato mainframe e diversi terminali collegati ad esso. Questo perché non esistevano ancora i personal computer e quindi il mainframe era un oggetto dalle dimensioni che potevano andare da quelle di una lavatrice a quelle di un armadio a sei ante. Anche il sistema minimo era costituito da un elaboratore collegato ad un terminale attraverso la linea seriale.
Nel caso di un collegamento locale, ad esempio all' interno di un' azienda, elaboratore e terminale/i erano collegati tramite un cavo seriale RS232

Il collegamento poteva anche essere remoto, cioè attraverso la linea telefonica, mediante l' interposizione di due modem.

Perché la linea era "seriale"? Perchè i dati dovevano essere comunque serializzati, sopratutto se si utilizzava un modem. Quindi, in buona sostanza, la linea seriale era il collegamento per eccellenza fra un terminale ed un elaboratore.

Il terminale

Il terminale è un oggetto composto da una tastiera, da un monitor e da tutta una circuiteria interna che fa funzionare il monitor come ricevitore e visualizzatore di caratteri (più avanti sono stati anche prodotti terminali grafici) e la tastiera come un trasmettitore. In pratica quando si preme un tasto sulla tastiera, il corrispondente codice del tasto premuto (i famosi codici ASCII) viene inviato attraverso la linea di trasmissione e, se viene ricevuto un carattere dalla linea di ricezione ,questo viene visualizzato nello schermo accanto all' ultimo carattere ricevuto. L' elaboratore può inviare al terminale non solo caratteri visualizzabili ma anche caratteri di controllo per esempio per cancellare lo schermo, oppure per andare a capo o spostare il cursore di una linea più in basso e tanto altro.

Terminale e microcontrollore

Utilizzare il collegamento seriale è estremamente utile sopratutto per la sperimentazione con i microcontrollori. Questo perché collegare un terminale ad un microcontrollore è un' operazione semplice e mette a disposizione una periferica di visualizzazione e di ingresso veramente potente. Non è strano vedere un micro che controlla un display LCD e che magari gestisce un testierino numerico. Bene, tramite la seriale possiamo immaginare di avere a disposizione un display di almeno 25 righe di 80 caratteri e, non un tastierino numerico, ma di una vera tastiera alfanumerica. Il tutto senza grattacapi, interfacciamenti, scansioni di matrici quant' altro. Sono sufficienti due piedini del micro, un minimo di circuiteria (tra l' altro molto semplice da realizzare) ed il gioco è fatto!
Supponiamo ad esempio di volere fare esperimenti sui convertitori A/D. In tal caso dovremmo per forza avere una qualche sorta di display dove visualizzare la misura effettuata. Oppure se vogliamo fare esperimenti con una RAM esterna dovremmo in qualche modo visualizzare dei dati, dei risultati. Bene, con la linea seriale abbiamo un terminale a disposizione, praticamente gratis perché il computer con cui in questo momento stiamo leggendo questo articolo può funzionare come terminale.

Il programma di emulazione di terminale

Se si usa Windows il famoso hyperteminal è il classico programma di emulazione di terminale. Di programmi ce ne sono molti, uno che uso con soddisfazione è RealTerm che, oltre ad emulare il terminale, può fare tante altre cose utili per lo sperimentatore. Ne consiglio vivamente l' utilizzo.
Hyperterminal è sparito con l' uscita di windows 7 ma, se avete ancora una copia di winXP, potete semplicemente copiarlo così com' è dalla cartella programmi\Windows NT\hypertrm.exe sul PC con WIndows 7. Funziona benissimo.

L' interfaccia fisica

Alcuni anni addietro i PC avevano a bordo una o due porte seriali RS232, oggi ci sono gli adattatori da USB a RS232. Quindi per collegare una scheda con un micro al PC tramite la porta seriale è sufficiente un adattatore e, dal lato micro, una circuiteria che adatti i livelli di tensione del micro ad i livelli richiesti dalla RS232.
Si può utilizzare uno schema come quello che a suo tempo ho postato nell' articolo Switch Control Unit:

SCU_RS232.jpg

SCU_RS232.jpg

Ricordandosi però che ST202 funziona con tensione a 5V. Per quanto riguarda il PIC18F47J53 non ci sono problemi per l' uscita TX del micro, in quanto un livello di 3,3V è considerato come un "1" logico dall' ingresso T1IN dello circuito sopra. Non ci sono neanche problemi per l' ingresso RX del micro in quanto è +5V tolerant e non si danneggia. Se si usa un micro o un pin che non ha questa caratteristica è sufficiente interporre un circuito come questo per adattare il livello.

Il circuito utilizzato in questo articolo

Per la mia sperimentazione ho utilizzato il convertitore USB-seriale che ho descritto in questo articolo collegato secondo questo schema elettrico:

Come si può vedere il circuito è di una semplicità disarmante. Il PIERIN lo alimento dalla sua presa USB ed il convertitore montato su una basetta è già alimentato di suo sempre tramite USB. La foto rende ancora meglio l' idea della semplicità

pierin_seriale.jpg

pierin_seriale.jpg

A destra c'è il convertitore montato sul suo basettino e, inserito fra due breadboard, c'è la scheda di sviluppo.

Nota: preferisco utilizzare due breadboard affiancate (ed incastrate fra loro) per avere bene disponibili i collegamenti della scheda di sviluppo e due file su cui disporre gli eventuali circuiti integrati per le sperimentazioni. Queste breadboards costano poco e due affiancate danno tanta libertà e possibilità di collegamenti.

Il programma

Lo scopo di questo programma è quello di illustrare l' inizializzazione minima della EUSART1 del microcontrollore, utilizzare le funzioni di output su stdout della libreria standard del C stdio.h, ed infine realizzare la funzione di echo sulla seriale.
Il compilatore C18 reindirizza automaticamente le funzioni di output (che nel C fanno riferimento al dispositivo stdout) alla seriale 1. Nella C18 User Guide ci sono le indicazioni su come reindirizzare l' output verso una funzione definita dall' utente (interessante per gli usi che implementano la classe CDC della USB) o verso un' altra seriale. Ho voluto includere l' esempio di utilizzo della printf perché questa funzione è estremamente comoda da utilizzare e non solo per simulare l' output su terminale ma proprio per comunicare con un terminale fisico.
Il progetto è stato creato partendo dal progetto di base descritto in questo articolo. procediamo con l' analisi di tutte le sue parti.

Inizializzazione

Per prima cosa accendiamo il PLL in modo da far funzionare il micro alla velocità massima (come già spiegato in questo articolo che parla del programma Demo)

  char ch;
  
  // Fa partire il PLL.
  // Anche se viene selezionato tramite i bit di configurazione
  // il suo funzionamento non è automatico. Ha bisogno di un comando.
  OSCTUNEbits.PLLEN = 1;
  // Attende abbastanza tempo per far stabilizzare il PLL
  Delay1KTCYx(100);
  // Da ora in poi abbiamo il PLL funzionante ed il micro con il turbo.

La dichiarazione iniziale char ch; ci servirà più avanti quando vedremo il ciclo infinito di funzionamento. Subito dopo c'è la parte che inizializza i pin di I/O dell' USART1 (RC6 e RC7)

  // -------- Inizializzazione delle periferiche --------
  
  EUSART1_deInit();
  // Inizializza i pin di I/0
  TRISCbits.TRISC7 = 1; // Pagina 345 data sheet
  TRISCbits.TRISC6 = 0;

Anche in questo caso ho chiamato per prima cosa la funzione (de me scritta) di de-inizializzazione che vedremo più avanti. Quello che interessa notare è che è necessario inizializzare i PIN di I/O come chiaramente descritto nel data sheet a pagina 345. in effetti il pin RC7 deve essere configurato come ingresso (ricezione dei dati seriali) ed il pin RC6 come uscita (uscita dei dati seriali). Vediamo ora il resto dell' inizializzazione

  // Inizializzazione Baud rate generator a 16 bit
  // Si calcola ora il valore da mettere nel registro del baud rate con
  // la formula che si trova a Pagina 349 data sheet.
  // Usando il generatore veloce ed il registro a 16 bit la formula è:
  // SPBRG = CLOCK_FREQ/(4 * (BAUD_RATE + 1)) 
  // che da come risultato 0x04E2
  SPBRG1 = 0xE2;
  SPBRGH1 = 0x04;
  BAUDCON1bits.BRG16 = 1;   // Seleziona modalità a 16 bit
  TXSTA1bits.BRGH = 1;      // Seleziona alta velocità
  
  // Inizializzazione trasmettitore e ricevitore
  TXSTA1bits.SYNC = 0;  // Pagina 355 data sheet
  RCSTA1bits.SPEN = 1;

Per prima cosa, come dice il datasheet, bisogna inizializzare il baud rate generator. In questo caso viene utilizzato in configurazione a 16 bit e ad alta velocità, quindi la formula per calcolare il valore da inserire nei registri SPRG1 (paret bassa) e SPRGH1 (parte alta) è la seguente (pagina 349 del data sheet):
n = \frac{F_{OSC}}{4\cdot (BaudRate + 1)}
Dove:

  • n è il valore da inserire nel registro a 16 bit del baud rate generator
  • FOSC è la frequenza di clock del sistema. Nel nostro caso 48 MHz.
  • BaudRate è la velocità di comunicazione che desideriamo impostare.

Nel programma ho selezionato la velocità di 9600 baud quindi la formula diventa:
n = \frac{48000000}{4\cdot (9600 + 1)}=1249,86 che arrotondiamo a 1250 cioè 0x04E2
che scriviamo nel registro del baud rate generator

  SPBRG1 = 0xE2;
  SPBRGH1 = 0x04;

le ultime due istruzioni servono per selezionare appunto la modalità con contatore a 16 bit (più precisa di quella ad 8 bit) e la modalità ad alta velocità.
In questo modo la nostra EUSART è configurata con 9600 baud, nessuna parità, 8 bit di dati ed un bit di stop. La cosa più semplice del mondo.
Non usiamo interrupt, saltiamo l' inizializzazione delle eventuali interrupt e non ci resta che attivare trasmettitore e ricevitore.

  // -------- Attivazione delle periferiche --------
  TXSTA1bits.TXEN = 1;  // Attiva il trasmettitore
  RCSTA1bits.CREN = 1;  // Attiva il ricevitore

Prima del ciclo infinito di funzionamento

Piace spendere due parole sulle prossime istruzioni, non tanto per le istruzioni in se, quanto per il fatto che dopo le inizializzazioni e prima di entrare nel ciclo infinito di funzionamento questo è il posto dove in cui va inserita quella parte di programma che deve essere eseguita una sola volta.
Capita spesso, magari anche solo per fare delle prove, di dover scrivere programmi che fanno un qualcosa e poi si fermano. Ovviamente un microcontrollore non si ferma mai e quindi questo è il posto giusto per inserirci le prove facendolo seguire da un ciclo di funzionamento vuoto come spiegato in questo articolo.
Mai terminare il main senza un ciclo infinito di funzionamento!
Nel nostro caso fare scrivere la famosa scritta "Hello, World!" seguita da un' altra frase.

  // -------- Codice che deve eseguito una solo volta --------
  printf ("\r\n\n\n\n");
  printf ("Hello, World!\n\r");
  printf ("PIERIN PIC18 manages the serial port!\n\r");

Il ciclo infinito di funzionamento

Ed eccoci arrivati al punto in cui implementiamo la funzione di echo. In pratica è una stupidata: si ritrasmette il carattere che si riceve realizza coì una sorta di echo. Il risultato sarà quello di vedere semplicemente visualizzati sul terminale i caratteri che vengono premuti sulla tastiera.

  // -------- Ciclo infinito di funzionamento --------
  for(;;)
  {
    // Inserire il programma qui.

    // Attende che vi sia un carattere disponibile dalla seriale
    // Se il carattere è disponibile il bit RC1IF va ad 1
    while(!PIR1bits.RC1IF);
    // Legge il carattere dal registro di ricezione
    ch = RCREG1;
    
    // Aspetta che il registro di trasmissione sia vuoto
    // Quando è vuoto il bit TX1IF va ad 1
    while(!PIR1bits.TX1IF);
    // Scrive il carattere nel registro di trasmissione
    TXREG1 = ch;

  }

Per accorgersi che c'è un carattere ricevuto nel registro di ricezione si guarda lo stato del bit RCnIF (n vale 1 perché usiamo la EUSART1) che poi + lo stesso che genera l' interrupt se questa è abilitata. Stessa cosa per la trasmissione del carattere: il bit TXnIF indica quando il registro di trasmissione è vuoto.

La funzione di de-inizializzazione

Anche se non sarebbe necessaria io l' ho scritta ugualmente anche perché può essere di aiuto, anche solo guardandola, per capire quali sono i registri che fanno riferimento alla periferica. Ovviamente diventa utile per de-inizializzare la periferica nel caso si dovesse riconfigurare all' interno del programma.

//------------------------------------------------------------------------------
// Funzione per la de-inizializzazione della USART1
void EUSART1_deInit(void)
{
  // De-inizializza il baud rate generator
  BAUDCON1 = 0x00;
  SPBRG =  0x0000;
  // De-inizializza il trasmettitore
  TXSTA1 = 0x00;
  PIE1bits.TX1IE = 0; // Interrupt enable bit
  PIR1bits.TX1IF = 0; // Interrupt flag
  IPR1bits.TX1IP = 0; // Interrupt priority
  // De-inizializza il ricevitore
  RCSTA1 = 0x00;
  PIE1bits.RC1IE = 0; // Interrupt enable bit
  PIR1bits.RC1IF = 0; // Interrupt flag
  IPR1bits.RC1IP = 0; // Interrupt priority
}

Usiamo il bootloader

Una volta compilato tutto il programma con target RELEASE si ottiene un file .hex che si può caricare con il bootloader dentro la scheda. Verificare che sia definito il simbolo

#define USE_HID_BOOTLOADER

contenuto nel file configurazione.h altrimenti il programma non funzionerà.
Nota: In C non è per forza necessario assegnare un valore ad un simbolo quando lo si definisce, lo si può definire e basta. I simboli semplicemente definiti (anche senza valore) servono per le compilazioni condizionate e sono utilizzati dalle direttive #ifdef (controlla se il simbolo esiste) e #ifndef (controlla se il simbolo non esiste). Molte volte queste direttive sono sufficienti per le compilazioni condizionate.
Lanciamo quindi il bootloader che rileverà la presenza della scheda. Se non la rileva premere il tasto di RESET sulla scheda del PIERIN. Comparirà la seguente schermata.

bootloader_connesso.jpg

bootloader_connesso.jpg

Premendo il simbolo della cartella o selezionando "File->Import Firmware Image" si seleziona il file .hex della cartella Output del progetto (nel mio caso l' ho chiamato "seriale"). La schermata diventerà

pierin_seriale_loaded.jpg

pierin_seriale_loaded.jpg

Per programmare il micro sarà sufficiente selezionare "Program->Erase/Program/verify Device" e partirà la programmazione. Al termine la schermata sarà questa

pierin_seriale_prog.jpg

pierin_seriale_prog.jpg

Fuoco alle polveri!

Per far partire il programma dobbiamo:

  • Scollegare la presa USB oppure rimuovere il Jumper JP1
  • Aprire il programma di terminale, selezionare la porta come 9600 baud, non parity, 8 bit, 1 stop no handshake e connettere
  • Ricollegare il cavo USB o reinserire il jumper JP1

Il risultato sarà questo

pierin_serial_output.jpg

pierin_serial_output.jpg

L' ultima riga visualizzata (quella piena di asdf e caratteri in libertà) è stata fatta dal sottoscritto pacioccando sulla tastiera per provare la funzione di echo.

Nota: Dato che questo programma necessita di un collegamento seriale e che questo potrebbe essere un problema si può dapprima caricare il file seriale.hex per verificarne la funzionalità e poi in un secondo tempo lavorare sopra il sorgente.
Nel caso si volesse ricaricare il programma di demo questo è disponibile per il download. Il suo nome è demo.hex

Conclusioni

Questo articolo è servito come semplice esempio di come utilizzare con poche istruzioni la linea seriale. La potenzialità di questa interfaccia è fuori discussione e quindi penso che possa diventare utile per la sperimentazione. A dire il vero io non scrivo (quasi) mai un programma o realizzo un circuito con un microcontrollore che non abbia una linea seriale. E' un qualcosa di utile e potente e che uso di frequente, sopratutto per il debuggind del programma. Nel caso del PIERIN utilizzato con il bootloader direi che è indispensabile per inserire all' interno del programma dei punti in cui si visualizza il valore di una variabile, oppure per dare indicazione che è stata fatta una cosa piuttosto che un altra. Senza emulatore la linea seriale è la migliore amica del microcontrollista.

Scusandomi in anticipo per eventuali errori e/o imprecisioni (sarò grato a coloro che me le faranno notare) non mi resta che augurare a tutti
BUONA SPERIMENTAZIONE!

Il programma completo

// File di definizione dei registri del micro. 
#include "p18f47j53.h"

// Inclusione file delle funzioni di delay
#include "delays.h"

// Inclusione file satndard I/O
#include "stdio.h"

// File di configurazione dei fuses
#include "configurazione.h"   

// Mappatura delle interrupt
#include "mappa_int.h"

// Header del main
#include "main.h"


//------------------------------------------------------------------------------
// Variabili globali
//------------------------------------------------------------------------------
#pragma udata


//------------------------------------------------------------------------------
// Funzione di servizio delle interrupt ad ALTA priorità
//------------------------------------------------------------------------------
#pragma code
#pragma interrupt highPriorityInterrupt
void highPriorityInterrupt()
{
  // Verifica quale flag ha causato l' interrupt
  // Esegui la parte di codice di servizio dell' interrupt
  // Azzera il flag che ha causato l' interrupt
  // ...
}

//------------------------------------------------------------------------------
// Funzione di servizio delle interrupt a BASSA priorità
//------------------------------------------------------------------------------ 
#pragma interruptlow lowPriorityInterrupt
void lowPriorityInterrupt()
{
	// Verifica quale flag ha causato l' interrupt
  // Esegui la parte di codice di servizio dell' interrupt
  // Azzera il flag che ha causato l' interrupt
  // ... 
  
} 

//------------------------------------------------------------------------------
// Prototipi delle funzioni
//------------------------------------------------------------------------------
#pragma code
void EUSART1_deInit(void);

//------------------------------------------------------------------------------
// Funzioni
//------------------------------------------------------------------------------
#pragma code

//------------------------------------------------------------------------------
// Funzione per la de-inizializzazione della USART1
void EUSART1_deInit(void)
{
  // De-inizializza il baud rate generator
  BAUDCON1 = 0x00;
  SPBRG =  0x0000;
  // De-inizializza il trasmettitore
  TXSTA1 = 0x00;
  PIE1bits.TX1IE = 0; // Interrupt enable bit
  PIR1bits.TX1IF = 0; // Interrupt flag
  IPR1bits.TX1IP = 0; // Interrupt priority
  // De-inizializza il ricevitore
  RCSTA1 = 0x00;
  PIE1bits.RC1IE = 0; // Interrupt enable bit
  PIR1bits.RC1IF = 0; // Interrupt flag
  IPR1bits.RC1IP = 0; // Interrupt priority
}

//------------------------------------------------------------------------------
// MAIN FUNCTION
//------------------------------------------------------------------------------
void main(void)
{
  char ch;
  
  // Fa partire il PLL.
  // Anche se viene selezionato tramite i bit di configurazione
  // il suo funzionamento non è automatico. Ha bisogno di un comando.
  OSCTUNEbits.PLLEN = 1;
  // Attende abbastanza tempo per far stabilizzare il PLL
  Delay1KTCYx(100);
  // Da ora in poi abbiamo il PLL funzionante ed il micro con il turbo.
  
  // -------- Inizializzazione delle periferiche --------
  
  EUSART1_deInit();
  // Inizializza i pin di I/0
  TRISCbits.TRISC7 = 1; // Pagina 345 data sheet
  TRISCbits.TRISC6 = 0;
  
  // Inizializzazione Baud rate generator a 16 bit
  // Si calcola ora il valore da mettere nel registro del baud rate con
  // la formula che si trova a Pagina 349 data sheet.
  // Usando il generatore veloce ed il registro a 16 bit la formula è:
  // SPBRG = CLOCK_FREQ/(4 * (BAUD_RATE + 1)) 
  // che da come risultato 0x04E2
  SPBRG1 = 0xE2;
  SPBRGH1 = 0x04;
  BAUDCON1bits.BRG16 = 1;   // Seleziona modalità a 16 bit
  TXSTA1bits.BRGH = 1;      // Seleziona alta velocità
  
  // Inizializzazione trasmettitore e ricevitore
  TXSTA1bits.SYNC = 0;  // Pagina 355 data sheet
  RCSTA1bits.SPEN = 1;
  
  // -------- Selezione ed abilitazione delle interrupt --------
  
  
  // -------- Attivazione delle periferiche --------
  TXSTA1bits.TXEN = 1;  // Attiva il trasmettitore
  RCSTA1bits.CREN = 1;  // Attiva il ricevitore
  
  // -------- Codice che deve eseguito una solo volta --------
  printf ("\r\n\n\n\n");
  printf ("Hello, World!\n\r");
  printf ("PIERIN PIC18 manages the serial port!\n\r");
  
  // -------- Ciclo infinito di funzionamento --------
  for(;;)
  {
    // Inserire il programma qui.

    // Attende che vi sia un carattere disponibile dalla seriale
    // Se il carattere è disponibile il bit RC1IF va ad 1
    while(!PIR1bits.RC1IF);
    // Legge il carattere dal registro di ricezione
    ch = RCREG1;
    
    // Aspetta che il registro di trasmissione sia vuoto
    // Quando è vuoto il bit TX1IF va ad 1
    while(!PIR1bits.TX1IF);
    // Scrive il carattere nel registro di trasmissione
    TXREG1 = ch;

  }
}
4

Commenti e note

Inserisci un commento

di ,

Non ho fatto niente. Nel manuale del C18 è spiegato che di default la printf finisce su UART1.

Rispondi

di ,

Ciao! come hai fatto ad associare lo stream dei dati alla USART? Sapevo che per ambienti come Atmel Studio si usava qualcosa come: static FILE mystdout = FDEV_SETUP_STREAM(miafunzione_write, miafunzione_read, _FDEV_SETUP_RW); Poi si faceva l'associazione: stdout = stdin = &mystdout; Mentre nelle funzioni includevi semplicemente il puntatore in questo modo, lasciandole invariate al loro interno: int miafunzione_read(FILE *stream) e tutto automagicamente funzionava, su qualsiasi periferica pilotata da una funzione che utilizzava dei caratteri come input (preso dal manuale AVR-Gcc) Tutto questo è stato fatto qui da qualche parte in automatico?

Rispondi

di ,

Grazie, ho modificato il disegno. :)

Rispondi

di ,

Complimenti per l'articolo, volevo far notare che nello schema PIERIN-convertitore USBseriale i pin RC7 ed RC6 sono invertiti :) o meglio, le etichette RC6 ed RC7. Ciao!

Rispondi

Inserisci un commento

Per inserire commenti è necessario iscriversi ad ElectroYou. Se sei già iscritto, effettua il login.