Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/ognigiorno.com/wp-content/plugins/math-comment-spam-protection/math-comment-spam-protection.php:1) in /var/www/html/ognigiorno.com/wp-includes/feed-rss2.php on line 8
Non voglio certo avere la pretesa di dare la soluzione a questo dilemma, mi fa però piacere descrivere la soluzione che ho adottato, che mi sta facendo lavorare molto bene da parecchio tempo, con una buona efficienza nel tempo dedicato alla scrittura del codice. Riporto inoltre alcune informazioni che permettono sicuramente di farsi un’idea migliore e prendere una decisione.
I problemi da affrontare
La scelta non è dettata esclusivamente dal linguaggio in termini di formalismo e sintattico (altrimenti immagino che tutti andrebbero sul BASIC senza troppi dubbi). Le problematiche da affrontare riguardano
– l’ottimizzazione del codice scritto in termini di risorse impiegate (memoria RAM, Program memory, tempi di esecuzione)
– il controllo diretto delle componenti a basso livello (registri, funzionalità speciali,…)
– il controllo diretto sui tempi di esecuzione necessario quando si lavora in real time
– la gestione della memoria
– la riusabilità del codice scritto e la sua portabilità su controllori differenti
– l’ergonomia nella scrittura del codice
Cosa fa un compilatore
“Il compilatore si occupa di tradurre in codice binario (o codice macchina) il programma scritto nel linguaggio che esso interpreta”. Questo è quello che macroscopicamente si associa al concetto di compilatore. In realtà un buon compilatore fa molto di più, soprattutto in relazione a quanto il linguaggio è “distante” dalla macchina. Si parla di generazione del linguaggio, pensando anche alla reale evoluzione storica che hanno avuto i linguaggi di programmazione
Spesso si può quindi parlare di assembler e codice macchina come se fossero sinonimi.
Si capisce che un linguaggio 3GL porta delle caratteristiche che complicano la traduzione in linguaggio macchina, in particolare, le strutture tipiche di un linguaggio di programmazione:
Siccome questi linguaggi hanno l’obiettivo principale di mascherare al programmatore la gestione delle risorse a basso livello (gestione RAM e program memory), è il compilatore che si occupa di automatizzare la traduzione.
Ragioniamo per esempio alle variabili. Quando scriviamo un programma in assembler utilizziamo genericamente indirizzi di memoria per apoggiare dei valori. Se non creiamo intrecci, possiamo utilizzare la stessa locazione per scopi differenti in rami diversi del programma. Per comodità ci costruiamo una “mappa” per cui ci è chiaro l’utilizzo di ogni porzione di memoria.
In C ci limitiamo a dichiarare una variabile sapendo quale è il suo scope (la sua visibilità). La gestione della memoria (scegliere quale locazione, decidere con chi può essere condivisa, da quale ramo di codice può essere scritta, ….) è tutte ad opera del compilatore.
Ogni compilatore implementa un suo preciso meccanismo per effettuare tutte queste operazioni. È proprio qui la differenza dei compilatori.
Proviamo a semplificare il lavoro di un compilatore:
Per tutte queste gestioni il codice macchina prodotto da un compilatore contiene molte più istruzioni di quelle strettamente necessarie per eseguire le operazioni a basso livello.
Questo è il motivo per cui genericamente si dice “l’assembler è più performante del C”. Ci si riferisce allo strato di funzionalità base che il compilatore introduce, l’overhead prodotto dal compilatore.
Questo è necessario perché un linguaggio 3GL si propone come linguaggio indipendente dalla macchina fisica su cui viene eseguito. Questa “indipendenza” si paga.
Dopo questa semplificata descrizione, riassumo alcune caratteristiche dei due linguaggi:
Linguaggio C
Assembler
Indipendentemente dal linguaggio, scrivendo firmware sui controllori è necessario avere la conoscenza del processore con cui stiamo lavorando. Per applicazioni più semplici e avendo a disposizione una buona libreria, con il C possiamo facilmente cavarcela anche senza conoscere i dettagli dell’hardware
La mia scelta
Innanzi tutto va tenuto conto che da hobbista utilizzo strumenti non professionali. Non ho intenzione di spendere migliaia di euro quando con un po’ di lavoro in più (si tratta sempre di passione) posso ottenere ottimi risultati gratis.
I miei strumenti sono MPLAB ed il compilatore Picc di HI-TECH versione freeware.
A meno di intervenire pesantemente su parametri di configurazione del compilatore, avere a disposizione compilatori con buona ottimizzazione e avere una conoscenza del compilatore più dispendiosa della conoscenza del processore, ci sono molte operazioni che non sono di fatto possibili in C.
Cito un esempio: le routine di trasmissione seriale. Scritte in C e compilate per i controllori PIC baseline funzionano senza problemi. Compilando lo stesso codice su controllori di fascia diversa, il compilatore cambia i criteri di ottimizzazione (che sono poi legati a effettive differenze hardware) e smettono di funzionare. C’è una spiegazione. Nella comunicazione seriale (bit banging) è richiesta una precisa gestione del tempo. L’overhead introdotto dal compilatore nel secondo caso è tale per cui sarebbe richiesta una ricalibrazione dei ritardi introdotti nel codice.
Abbiamo un’alternativa, la stessa scelta che è stata fatta nel mondo dei computer. Ormai a costi bassi possiamo trovare controllori sempre più potenti, che offrono come funzionalità base quasi tutto quello che serve, con memoria pressoché illimitata. Quindi per far lampeggiare un LED scriviamo un semplice programma in C e lo facciamo girare su un controllore 1 32bit dotato di porta USB, Ethernet, con 32k di program memory, 256k memoria RAM, gestione nativa del BUS I2C e SPI, 32 convertitori A/D, …
Forse è un po’ eccessivo? È comunque una possibilità. Con i computer hanno fatto così!
Io preferisco identificare il processore più adatto allo scopo e dimensionarlo correttamente, disegnare il circuito e dimensionare il tutto in base alle effettive esigenze.
Come linguaggio utilizzo insieme il C e l’assembler: ho costruito una buona libreria di funzioni base in assembler che utilizzo chiamandole da funzioni C. Solitamente il main lo scrivo in C, scrivo in C funzioni che non richiedono particolare ottimizzazione, uso le funzioni assembler e sto sempre attento alla percentuale di risorse utilizzate. Quando sforo, mi tocca sempre convertire in assembler alcune delle funzioni C.
Utilizzando la placca BTicino delle prese shuko ho installato la tastiera al muro con un lavoro praticamente invisibile, dalla resa altamente professionale. La tastiera utilizzata è di quelle a membrana con adesivo sul retro. Per l’installazione ho incollato la tastiera al supporto nero (quello su cui solitamente si incastrano gli interruttori o le prese) e applicato la placca. Il circuito è inserito nella scatola all’interno del muro.
Le tastiere a matrice le possiamo anche reperire in un qualsiasi mercatino, per esempio acquistando un vecchio telefono, o possiamo realizzarle noi stessi, collegando gli interruttori in uno schema righe/colonne.
La logica consiste nel raggruppare righe e colonne della tastiera e collegare tutti i terminali a porte di I/O del controllore. Le porte vanno quindi configurate in Input per le colonne e Output per le righe (o viceversa). Le linee in ingresso dovrebbero avere una resistenza di pull-up (o pull-down, se preferiamo. Il codice va poi scritto di conseguenza).
La routine che legge il tasto premuto deve ciclare sui piedini attestati alle righe, portando a livello basso uno alla volta. Leggendo le colonne in assenza ti tasto premuto, si leggerà 1 su tutti i piedini. Quando la lettura è 0 significa che è stato premuto il tasto corrispondente all’incrocio riga/colonna (riga portata bassa, colonna letta).
Le tecniche per ciclare sulle righe sono differenti: nell’esempio da me riportato vengono modificate una per una direttamente. In alternativa si può utilizzare l’operatore si shift. Dipende da quali sono i terminali a cui sono collegate. Ne dobbiamo tenere conto (se possibile) in fase di progettazione. Non sempre è possibile perché ormai i controllori hanno molti piedini dedicati a funzioni speciali (UART, SPI, I2C, convertitori A/D, PWM, …) per cui non sempre riusciamo dedicare alla tastiera piedini contigui della stessa porta.
Immediatamente dopo aver effettuato la lettura va inserito un piccolo ritardo come antirimbalzo.
Nel codice sorgente che ho allegato vediamo un altro controllo sulla pressione, inserito solo per un tasto, ma utilizzabile tranquillamente per tutti. Associato alla pressione di un tasto ho due differenti codici di ritorno in caso di pressione breve o prolungata.
unsigned char ReadKey (void) {
unsigned char tt;
for (;;) {
PORTC = 0xff;
RC5 = 0;
if (RB5 == 0)
return 0x0b;
if (RB4 == 0)
return 0;
if (RB3 == 0) {
tt = 0;
while (!RB3) {
DelayMs (2);
tt++;
if (tt > 200)
return 0x1a;
}
return 0x0a;
}
RC5 = 1;
RC4 = 0;
if (RB5 == 0)
return 9;
if (RB4 == 0)
return 8;
if (RB3 == 0)
return 7;
RC4 = 1;
RC3 = 0;
if (RB5 == 0)
return 6;
if (RB4 == 0)
return 5;
if (RB3 == 0)
return 4;
RC3 = 1;
RC1 = 0;
if (RB5 == 0)
return 3;
if (RB4 == 0)
return 2;
if (RB3 == 0)
return 1;
}
...
c = ReadKey();
DelayMs (200);
Il modulatore
Per la comunicazione viene utilizzato un LED emettitore di infrarossi, un led che emette luce ad una frequenza non visibile. Questa luce viene modulata in modo da trasmettere in sequenza (trasmissione seriale) le informazioni (bit).
Nell’ambiente esistono moltissime fonti di infrarossi (la luce del sole, le lampadine, …) per questo motivo le informazioni vengono solitamente modulate su una portante di qualche decina di kHz (i valori utilizzati vanno da 30 a 60).
Vediamo cosa significa:
La trasmissione viene miscelata alla frequenza della portante. Il risultato è visibile in figura. In questo modo il ricevitore è in grado di ricostruire il segnale originario tramite un filtro passa-banda centrato sulla frequenza della portante. Si eliminano in questo modo disturbi ed interferenze.
A semplificazione del circuito di decodifica sono disponibili in commercio componenti che racchiudono ricevitore e filtro (TSOP12xx, dove xx indica la frequenza della portante. TSOP1236 riceve a 36kHz, TSOP1248 a 48kHz, e così via). Questi ricevitori hanno 3 piedini: +5v, GND e signal out. Quest’ultimo si può collegare direttamente al piedino di ingresso del nostro controllore.
La trasmissione
A questo punto possiamo concentrarci esclusivamente sui dati che vengono trasmessi, pensando al protocollo di comunicazione e a come vengono codificati i simboli (1 e 0). Purtroppo non esiste uno standard universale, ma ogni produttore ha sviluppato il suo.
Mi voglio concentrare in questo articolo sul protocollo adottato da Philips (il mio telecomando), tratterò a parte una raccolta di protocolli differenti. Il protocollo qui descritto è uno dei diversi implementati da Philips. Il nome corretto è RC-5.
Codifica
Il primo elemento da analizzare è come vengono rappresentati i due simboli 1 e 0. è in base a questo che va scritto l’algoritmo per il PIC (assembler o C).
La portante utilizzata è 36kHz
Modulazione:
Protocollo
3 bit di start – 5 bit indirizzamento – 7 bit comando
I primi 3 bit vengono utilizzati per segnalare l’inizio trasmissione: i primi due hanno valore fisso a 1, il terzo cambia ogni volta che viene lasciato e ripremuto un pulsante. Serve per distinguere tra una pressione prolungata dello stesso tasto e due pressioni successive. Questo bit viene chiamato Toggle (T)
L’indirizzamento è utilizzato per identificare il dispositivo da controllare (televisore, videoregistratore, …)
Il comando identifica il tasto premuto.
In figura vediamo come è composta la stringa che vene trasmessa alla pressione di ogni tasto. I valori dell’esempio sono: Indirizzo = 2, comando = 13
Passiamo alla pratica.
Manca solo il codice. L’esempio qui allegato decodifica il telecomando e trasmette sulla seriale (RS232) i due valori di comando e device. Serve come esempio, per comprendere il funzionamento della routine di decodifica. Questa può essere infatti utilizzata per qualsiasi impiego. Il progetto allegato è scritto e compilato per il 12F519, ma il codice è utilizzabile per tutti i PIC baseline. Con la versione free del compilatore picc di HI-TECH (quello che utilizzo io) vanno modificati i tempi per controllori di fascia più alta (l’ottimizzazione non è il massimo). In alternativa va riscritto il codice in assmbler. Per ora l’ho utilizzato solo su questo controllore, ma appena scriverò l’assembler, lo posterò.
La codifica/decodifica viene quasi sempre realizzata utilizzando una coppia di circuiti integrati (HT12E e HT12D o equivalenti), dedicati allo scopo, abbinati ad un trasmettitore e un ricevitore radio.
Il trasmettitore invia un codice composto da 12 bit. I radiocomandi hanno solitamente un dip switch che permette di selezionare il codice. Il codice identico deve essere impostato sul ricevitore, per permettere il riconoscimento.
Radiocomandi a più canali utilizzano una parte dei 12 bit per identificare il canale: per N canali si ha un indirizzo a 12-N bit.
Di seguito lo schema a blocchi tipico del radiocomando:
La velocità di trasmissione solitamente utilizzata è di 1000 baud, la codifica è la seguente:
Avendo queste informazioni siamo quindi in grado di realizzare un radiocomando con codifica e decodifica tramite PIC.
Per la parte radio utilizziamo i moduli trasmettitore e ricevitore della Quasar che permettono con pochi euro di ottenere ottimi risultati. Questi moduli possono essere acquistati presso qualsiasi distributore di componenti elettronici, anche se io preferisco risparmiare ed acquistare direttamente sul sito del produttore.
L’utilizzo dei moduli richiede veramente poco. Entrambi ricevitore e trasmettitore hanno 4 connessioni: positivo e negativo di alimentazione, antenna e dati.
Ecco la routine del trasmettitore (la frequenza di oscillazione da me utilizzata è di 8Mhz).
void TxChar(unsigned char c) {
unsigned char bitno;
INIT_PORT;
bitno = 4;
do {
TxData = 0;
delay_2Us (165);
if(c & 1)
TxData = 0;
else
TxData = 1;
c = (c >> 1) | 0x80;
delay_2Us (164);
TxData = 1;
delay_2Us (165);
} while(--bitno);
}
Come si vede dal codice, la routine effettua la trasmissione dei soli 4 bit meno significativi del parametro in ingresso. Questo per maggiore comodità, essendo a 12 bit il codice che deve essere trasmesso.
Il ricevitore è un po’ più complesso, a causa dei disturbi tipici del canale radio. Ho quindi implementato una sorta di macchina a stati finiti con due stati: in attesa e in ricezione:
int RxRadio(void) {
unsigned char bitno, tmp, width, w0;
state0:
width = 0;
d1 = 0;
d2 = 0;
bitno = 0;
while(!RxDt)
continue; /* wait for start bit */
while (RxDt) {
delay_2Us(4);
width++;
if (width > 45)
goto state0;
}
if (width < 30)
goto state0;
state1:
width = 0;
w0 = 0;
tmp = 1;
while (!RxDt){
delay_2Us(4);
w0++;
if (w0 > 90)
goto state0;
}
while (RxDt) {
delay_2Us(4);
width++;
if (width > 90)
goto state0;
}
if (width < 20)
goto state0;
if (width > w0)
tmp = 0;
if (bitno > 7)
d1 = (d1 >> 1) | (tmp << 7);
else
d2 = (d2 >> 1) | (tmp << 7);
bitno ++;
if (bitno == 12)
return width;
goto state1;
}
Per ridurre problemi derivanti da disturbi o errori di ricezione, il decodificatore si aspetta di ricevere gli stessi dati per tre volte consecutive. In caso contrario non effettua la decodifica. La trasmissione avviene quindi correttamente se chiamiamo 3 volte con gli stessi valori la TxChar.
Di seguito un esempio che illustra come poter effettuare la trasmissione di tutte le combinazioni possibili in sequenza, utile se abbiamo perso il telecomando e vogliamo individuare il codice per duplicarlo.
void InitTx(void) {
TxData = 1;
delay_2Us (165);
TxData = 0;
}
void EndTx (void) {
TxData = 0;
DelayMs(10);
}
...
for (j=0; j<16; j++)
for (i=0; i<256; i++)
for (t=0; t<4; t++) {
InitTx();
TxChar (i);
TxChar (i>>4);
TxChar (j);
EndTx();
}
for (;;);
va considerato che la trasmissione completa dura:
(1000 baud x 12 bit + 10 ms + 330 us) x 3 volte = 67 ms per ogni codice trasmesso
2^12 combinazioni x 36 ms = 4096 x 67 ms = 274,432 s = 4,57 minuti
]]>
Costruire circuiti o semplici robot ci porta sempre ad avere bisogno delle stesse periferiche: schede display, tastiera, RTC, …
Lavorando in questo modo, per ogni progetto dobbiamo occuparci della progettazione della scheda madre e di come interagisce con le sue periferiche.
Il protocollo qui descritto offre alcuni vantaggi che lo rendono applicabile anche sui controllori pi piccoli. Ecco le caratteristiche principali:
Formato dei messaggi
Comando: [stx][vli][1][cmd][addr]…data…[crc][etx]
Risposta: [stx][vli][0][cmd][addr]…data…[crc][etx]
stx: 0x02
vli: Variable length indicator
cmd: codice che identifica il comando da inviare o a cui si risponde
addr: indirizzo di destinazione o sorgente, in caso di risposta
data: eventuali dati aggiuntivi. Dipende dal comando.
crc: controllo sui dati trasmessi. Io ho implementato XOR su tutti i byte
Etx: 0x03
]]>Escludiamo il PC, per cui oggi preferiamo metodi più “nobili” come l’USB o Ethernet, considerando anche che difficilmente si riesce ancora a reperire un computer dotato di porta RS232.
Il trasferimento seriale
La trasmissione seriale avviene inviando in sequenza su un solo filo i singoli bit che compongono i dati da inviare. Solitamente quando due controllori devono comunicare si utilizzano due linee, una in ingresso (RX) e l’altra uscita (TX).
Se vogliamo avvicinarci allo standard RS232, vediamo quali sono le regole che lo governano:
La durata di ciascun bit è determinata dalla velocità di trasferimento. Per esempio: 9600 baud significa che ogni simbolo ha una durata di 1/9600 secondi = 104 us.
Adesso quasi tutti i controllori PIC contengono il modulo UART (universal asynchronous receiver/transmitter), in questo caso si realizza la comunicazione sfruttando il modulo, utilizzando i registri che lo controllano.
Può comunque capitare di dover implementare la comunicazione lavorando direttamente con piedini I/O generici. Per esempio se utilizziamo i controllori più piccoli, o se dobbiamo avere più di un’interfaccia su linee distinte.
Vediamo quindi ora l’implementazione della trasmissione.
La criticità principale deriva dalla precisione che dobbiamo avere nella gestione dei tempi. Per questo motivo è preferibile lavorare in assembler.
Io utilizzo MPLAB di Microchip e il compilatore C di HITEC (picc). Le parti di codice in assembler le integro nei programmi C, sfruttando il linker di questo compilatore: basta aggiungere il sorgente .as nel progetto, e fa tutto da solo.
La versione freeware di questo compilatore non è affatto ottimizzata, quindi siamo realmente costretti a lavorare in assembler per i controllori midrange, mentre con i baseline ho avuto buoni risultati anche con il c (per esempio il 16F506 e il 12F519).
Partiamo dalla routine di trasmissione scritta in C:
void Putch(unsigned char c) {
unsigned char bitno;
TxData = 0; /* start bit */
bitno = 12; /* Rallenta la trasmissione, ma migliora la qualità*/
do {
delay_2Us (50);
if(c & 1)
TxData = 1;
else
TxData = 0;
c = (c >> 1) | 0x80;
} while(--bitno);
}
Per poi vedere la sua codifica in assembler:
PSECT text,class=CODE,local,delta=2
GLOBAL _Putch
GLOBAL ??_Putch
FNSIZE _Putch, 3, 1
#include "aspic.h"
#define Par ??_Putch
#define Tmp ??_Putch+2
#define Bitn ??_Putch+1
_Putch:
movwf Par
movlw 0x0c
movwf Bitn
bcf TX_port, TX_pin
loop:
; delay 1 bit
movlw bit_delay
movwf Tmp
delay:
nop
decfsz Tmp, f
goto delay
; copia il bit in uscita (ultimo bit dx)
btfss Par, 0 ; skip if set
goto set0
set1:
bsf TX_port, TX_pin
goto cont
set0:
bcf TX_port, TX_pin
cont:
; ruota a dx il byte, mettendo a 1 il più a sx
bsf STATUS, 0
rrf Par, f
decfsz Bitn, f
goto loop
bcf STATUS, 5
bcf STATUS, 6
return
Leggermente più complicata la ricezione. Considerando che la linea è alta a riposo, si reagisce appena si abbassa: presente il valore 0, aspetto metà del tempo di durata di un bit, in modo da posizionarmi nel centro e campiono il valore per otto volte.
Il codice in C
char Getch(void) {
unsigned char c, bitno;
for(;;) {
while(RxData) {
continue; /* attesa bit di start */
}
delay_2Us (25);
if(RxData)
continue; /* filtro i disturbi*/
bitno = 8;
c = 0;
do {
delay_2Us (50);
c = (c >> 1) | (RxData << 7);
} while(--bitno);
return c;
}
}
E qui l’assembler
PSECT text,class=CODE,local,delta=2
GLOBAL _Getch
GLOBAL ??_Getch, ?_Getch
FNSIZE _Getch, 3, 1
define Retv ??_Getch
#define Temp ??_Getch+2
#define Bitno ??_Getch+1
_Getch:
clrf Retv
; movlw 0x32
; movwf Temp
Start:
; decf Temp, f
; btfsc STATUS, 2
; goto Uscita
btfsc RX_port, RX_pin
goto Start
; delay mezzo bit
movlw half_delay
movwf Temp
delay1:
nop
decfsz Temp, f
goto delay1
; disturbo sulla linea: accapo!
btfsc RX_port, RX_pin
goto _Getch
movlw 0x08
movwf Bitno
bitRx: ; delay 1 bit intero
movlw bit_delay
movwf Temp
delay2:
nop
decfsz Temp, f
goto delay2
bcf STATUS, 0
btfsc RX_port, RX_pin
bsf STATUS, 0
rrf Retv, f
;finito?
decfsz Bitno, f
goto bitRx
Uscita:
movf Retv, w
bcf STATUS, 5
bcf STATUS, 6
return
Con queste due routines facciamo comunicare i controllori riducendo il numero di connessioni utilizzate. Per comunicare con un PC è necessario un adattatore di livelli. Il PIC infatti lavora con valori TTL (0 = 0V, 1 = +5V), mentre l’interfaccia RS232 utilizza valori EIA (0 = -12V, 1 = +12V). Solitamente si utilizza l’integrato MAX232.
Lavorando con i nuovi controllori PIC con oscillatore interno (senza quarzo) a 8Mhz, i risultati migliori li ho ottenuti a 9600 baud (i valori utilizzati nel codice che ho riportato).