PIC

Programmazione con Linux



Guida introduttiva alla programmazione assembler e C di PIC in ambiente GNU/Linux

Copyright (c) 2007 MASSIMO LUNARDON & WALTER LAIN

Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License".

Le porzioni di codice sono rilasciate con licenza GNU GPL, tranne il firmware dell'ICD2, che e' di proprieta' esclusva di Microchip Technology Incorporated.

Tutti i marchi sono registrati dai rispettivi proprietari.

Indice
Materiale
Introduzione
Questo documento e' stato prodotto allo scopo di fornire il riassunto di alcune serate relative alla programmazione di microcontrollori della famiglia PIC16 ottenuta con l'utilizzo di strumenti open source (OS).

Queste serate sono state organizzate per conto del
LUG di Vicenza, e si sono svolte presso il Victoria Station di Vicenza, nei giorni 4, 11, 18 Dicembre 2007.

Si inizia fornendo una descrizione di cosa e' un microcontrollore, le caratteristiche principali delle famiglie di Pic, per passare poi ad illustrare quelle dello specifico Pic adottato.

Si vedranno poi gli strumenti abitualmente utilizzati in ambito professionale in ambiente Windows; si confronteranno con gli strumenti disponibili in ambito OS per mostrare il livello di maturita' raggiunto da questi ultimi.

Infine saranno illustrati degli esempi pratici di programmazione sia in linguaggio Assembly (basso livello) che in C (alto livello). Se ne confronteranno le caratteristiche, le prestazioni e le difficolta'.
Torna all'indice


Microcontrollore
Un microcontrollore e' un dispositivo elettronico con un certo numero di ingressi ed uscite, che puo' essere programmato per svolgere delle funzioni che modificano lo stato delle uscite in base a delle logiche determinate dalle istruzioni impostate. Tali logiche possono dipendere dai livelli presenti agli eventuali ingressi piuttosto che da un clock che talvolta puo' essere interno.

Struttura generica di un MCU Microchip

In genere un microcontrollore dispone di una certa quantita' di memoria, che puo' essere utilizzabile per contenere le istruzioni che compongono il programma e dati che possono essere temporanei o meno.

Questa memoria sara' divisa almeno in due parti, una parte volatile (RAM) e una parte non volatile (OTP, EPROM, EEPROM, FLASH).
Torna all'indice


PIC
I microcontrollori Pic sono prodotti da
Microchip. Esistono diversi tipi di Pic che si differenziano per il tipo di core e per il tipo di moduli presenti al loro intero. Le due famiglie principali sono quelle con word a 16 o 14 bit.

Tutte le famiglie hanno delle caratteristiche in comune: il set base di istruzioni, l'architettura, la modalita' di programmazione.

I vari moduli disponibili possono essere di interfaccia (I2C, USART, USB), ingressi analogici (comparatori, convertitori A/D), ingressi digitali (contatori, timer, clock).
Torna all'indice


Caratteristiche
I Pic dispongono di una CPU RISC, che esegue un set base di 35 istruzioni (le famiglie piu' potenti hanno delle istruzioni aggiuntive). Ogni istruzione e' codificata in una sola word ed e' eseguita in un solo ciclo di clock , eccetto le istruzioni di branch che ne impiegano due.

L'esecuzione di una istruzione per ciclo di clock e' resa possibile da un meccanismo di pipeline.

I Pic dispongono di memoria volatile (RAM) e non volatile (FLASH, EEPROM): la RAM e' tipicamente utilizzata per l'esecuzione dei programmi, mentre la FLASH per memorizzare il codice o i dati.

Il Pic ha una architettura di tipo Harvard: bus dati e bus istruzioni sono distinti. Cio' permette di accedere alle istruzioni ed ai dati allo stesso istante, senza che si verifichino problemi di contesa del bus, rendendo il controllore piu' veloce nelle applicazioni che elaborano dati residenti in memoria.

Le due principali famiglie si differenziano per la dimensione della word adottata: 16 bit in un caso, 14 nell'altro. Il bus dati e' sempre a 8 bit. La famiglia a 16 bit e' identificata dalla sigla Pic18Xxxx, mentre la famiglia a 14 bit con la sigla Pic16Xxxx. Esistono anche altre famiglie, come Pic12Xxxx, dsPic30, ecc...

La struttura di base e' comune a tutta la famiglia, i singoli modelli si distinguono per il numero di PORT e la presenza o meno di altri moduli di periferiche.

Il Pic e' in grado di gestire degli interrupt, e' dotato di un watchdog con un proprio timer e le sue uscite sono in grado di supportare una corrente sufficiente per pilotare dei led. Generalmente i Pic contengo al loro interno una moltitudine di moduli, tanto che i pin del package non sono sufficienti per esportarne tutti i contatti elettrici; per questo motivo le uscite sono multiplexate, e devono essere opportunamente configurate per utilizzare un modulo piuttosto che un altro.

Un'altra caratteristica peculiare e' la capacita' di In-Circuit Serial Programing (ICSP) che permette di programmare un Pic gia' saldato nel circuito finale. Vedremo che questa caratteristica e' molto utile.
Torna all'indice


Il PIC 16F628A
Nei nostri esempi utilizzeremo il PIC16F628A.

Il PIC16F628A e' uno dei MCU prodotti da Microchip. Appartiene alla famiglia 16F, quindi e' un MCU a 8 bit, ed ha una memoria di tipo FLASH. Questa ultima caratteristica lo differenzia dalla famiglia 16C, che e' composta da dispositivi OTP la cui memoria non e' riscrivibile (One Time Programming - programmabili un'unica volta).

Il bus dati e' ad 8 bit, il bus che accede alla memoria programma e' a 14 bit.

Il modello PIC16F628A e' uno dei modelli "piccoli" della sua famiglia. Al suo interno contiene:

Struttura del PIC 16F628A

La memoria disponibile e' divisa in 3 parti: 2048 words di programma (FLASH), 224 bytes di RAM e 128 bytes di EEPROM (di fatto si tratta di una porzione di memoria FLASH, ma dal punto di vista del software cio' e' irrilevante). Oltre a questi spazi di memoria sono presenti un registro accumulatore W e diversi registri di configurazione. In questo tipo di Pic una word e' di 14 bit; un qualsiasi comando assembler, una volta codificato, occupa 1 word che contiene sia il codice dell'istruzione che il dato.

Struttura della memoria programma del PIC 16F628A

La memoria RAM e' suddivisa, in modo non simmetrico, in 4 banchi. Una peculiarita' e' che gli indirizzi da 70h a 7Fh sono comuni a tutti i banchi (cioe', qualsiasi banco si selezioni si punta sempre alle stesse locazioni), e questo puo' diventare utile in molti casi.

Anche la struttura della memoria e' comune a tutta la famiglia; la RAM e' sempre suddivisa in 4 banchi, di cui gli ultimi 16 bytes comuni (cambia la quantita' indirizzabile nei vari banchi), e la memoria programma e' composta di banchi da 2Kwords ciascuno. Il PIC16F628A dispone di un solo banco utilizzabile, mentre altri modelli della stessa famiglia arrivano fino a 4.

Struttura della memoria RAM del PIC 16F628A

Sono presenti 6 tipi differenti di interrupt:
Gli interrupt hanno due flag generali di attivazione, un master generale (GIE) ed un master degli interrupt periferici (PEIE), entrambi presenti sul registro INTCON. L'unico interrupt non periferico e' INT, quindi negli altri casi e' sempre necessario settare entrambi i flags per attivare gli interrupt. Quando avviene un interrupt, il programma attiva il flag corrispondente e salta alla locazione 4h, dove il programmatore puo' eseguire le routines del caso. Dopo ogni evento l'interrupt che l'ha generato deve essere riattivato manualmente.

Il codice che viene eseguito in seguito ad un interrupt viene anche detto vettore di interrupt.

I 3 timer sono fondamentalmente dei contatori, che incrementano il loro valore ad ogni ciclo e generano un interrupt al passaggio per lo zero, che avviene ogni volta che raggiungono il valore massimo e provocano un over-flow passando da 0xFF a 0x00. Ne consegue che per ottenere una corretta temporizzazione, occorrera' inserire nel contatore non il valore desiderato, ma il resto della sottrazione tra questo valore e 0xFF, oppure 0xFFFF nel caso del timer a 16 bit.

Sono presenti dei prescaler e dei postscaler per adattare meglio i timer alle esigenze dell'applicazione.

Sono presenti anche un timer di accensione, un watchdog e un sistema di rilevamento del brown-out.

L'oscillatore interno puo' lavorare alle frequenze di 4MHz (con precisione dell'1%) oppure 48KHz, e sostituisce egregiamente un oscillatore esterno (quarzo, RC o altro) in buona parte delle applicazioni realizzabili, rendendo al contempo disponibili 2 pin di I/O altrimenti occupati. Unica accortezza e' disabilitare il pin di reset (MCLR) nella configurazione, altrimenti l'oscillatore non funzionera'.

Un'altra cosa da tenere presente e' che i 2 pin di programmazione PGD e PGC (bit 6 e 7 della porta B: RB6 e RB7) non dovrebbero mai essere utilizzati come uscite, pena l'impossibilita' di riprogrammare il dispositivo dopo la prima programmazione.

Il linguaggio assembler del PIC e' composto da 35 istruzioni, comprendenti spostamento di registri e varie operazioni su interi a 8 bit, tra cui addizione, sottrazione, complemento a 2, AND logico, ecc... . Non sono presenti la moltiplicazione e la divisione, per cui andranno eventualmente scritte delle routines apposite, cosi' come per operazioni su dati a piu' di 8 bit o in virgola mobile (sulle application notes fornite da Microchip sono presenti tutte le funzioni necessarie).

Istruzioni assembler definite sul PIC 16F628A

Per ulteriori informazioni fare riferimento al Datasheet del MCU.
Torna all'indice


I tools
Per lo sviluppo degli esempi utilizzeremo del software Open Source.

I tools che utilizzeremo sono:
E bene che nel processo di installazione si segua l'ordine dell'elenco, in modo da soddifsfare le eventuali dipendenze.
Torna all'indice


Piklab
Piklab e' un IDE in grado di collegarsi a vari programmatori paralleli e seriali ed anche all'ICD2 Microchip. Puo' utilizzare gpasm per la parte assembler, e diversi compilatori C (liberi e commerciali) per la programmazione ad alto livello.

L'interfaccia presenta l'elenco dei files di progetto sulla sinistra, i files nella parte destra e i messaggi in basso.


Piklab, ambiente IDE

L'editor e' basato su Kate, e l'evidenziatura del codice puo' essere impostata secondo vari schemi.

Il programma e' in continuo sviluppo. Attualmente e' possibile utilizzare diversi programmatori e sembra sia presente un inizio di supporto al debug in-circuit (utilizzando un debugger tipo ICD2, e' possibile eseguire il software step-by-step sull'hardware, eseguendo cosi' un debug sia software che hardware

Il debug puo' essere eseguito anche utilizzando l'emulatore gpsim, e permette la visualizzazione dei vari registri di sistema, oltre alle variabili create in RAM; e' anche possibile forzare lo stato dei registri e modificare il contenuto delle variabili. Gpsim attualmente non supporta i dispositivi di tipo "A" (cioe' con moduli analogici), perlomeno della serie Pic16

Piklab, ambiente di debug con GpSim

Per chi avesse gia' dimestichezza con l'ambiente MPLAB Microchip, Piklab risulta abbastanza semplice da utilizzare, tenendo conto comunque che alcune macro e scorciatoie presenti in Mplab non sono riconosciute da Piklab.

Alcuni accorgimenti di cui tenere conto, pena l'impossibilita' di portare a termine la compilazione:
Questi problemi sono da imputare certamente alla giovinezza dei vari progetti, ma occorre tenerne conto per poter utilizzare l'ambiente OS senza problemi.
Torna all'indice


ICD2 Clone
Per una descrizione di questo circuito fare riferimento alla
sezione specifica.

Per poter utilizzare il programmatore, si deve configurare il sistema affinche' riconosca il device USB e crei un device ttyUSBx con i permessi opportini affinche' il dispositivo possa essere utilizzato da un account normale (non root!).

Per i sistemi che adottano il meccanismo di udev, si deve creare un file di rules ad-hoc.

Si dovra' copiare il seguente file (50-usb-ftdi.rules) in /etc/ude/rules.d/:

# udev rules file for FDTI USB-RS232 adapter
# $Id: max.rules,v 1.3 2007/02/15 22:12:10 gaufille Exp $
#
BUS!="usb", ACTION!="add", GOTO="myusb_rules_end"

# All devices
# Comment out if you are looking for product id rules
# and uncomment the next rule
SYSFS{idVendor}=="0403", MODE="666", GROUP="users"

# Ftdi device with product ID #6001 will be mounted
# on a node with "rwrwrw" rights for "users" group
SYSFS{idVendor}=="0403", SYSFS{idProduct}=="6001", MODE="666", GROUP="users"

LABEL="myusb_rules_end"


In questo modo ogni volta che sara' collegato il programmatore alla porta USB, il meccanismo di udev creera' un device /dev/ttyUSBx con permessi di lettura e scrittura per tutti e appartenente all'utente root e al gruppo users.
Torna all'indice


Il circuito dimostrativo
Al fine di rendere immediatamente visibile il funzionamento del PIC, e' stato realizzato un
semplice circuito dimostrativo.
Questo circuito e' composto da:
Sono inoltre presenti una resistenza da 100R, una da 10K, un condensatore da 100nF ed un connettore pin strip, utilizzati per la programmazione.

Il circuito dimostrativo (in questa versione e' presente anche una sezione di alimentazione)

Il funzionamento del circuito e' molto semplice:
Per la selezione dei pin occorre tenere conto di alcuni fattori:
Di conseguenza, sono stati scelti per i led i pin da RB0 a RB5 e i pin RA6 e RA7.

Torna all'indice


Il software dimostrativo
Il software creato per la dimostrazione e' da considerarsi a scopo didattico e decisamente poco ottimizzato, specialmente per quanto riguarda le dimensioni in memoria (per contro, risulta certamente piu' comprensibile di un analogo software ottimizzato, il che e' lo scopo finale di questo lavoro).

E' composto da diverse parti:
Va considerato che questo metodo di programmazione, suddividendo per argomento e funzione il programma in vari files, e' da preferirsi ad una gestione a file unico, in quanto risulta piu' facile da gestire (obiettivamente, riducendo il numero di righe presenti si facilita l'individuazione del codice cercato) sia durante la programmazione che durante il debug. E' anche possibile, in questo modo, salvare e recuperare con piu' facilita' le differenti release del software, e recuperare con maggiore facilita' porzioni di codice per riutilizzarle in altri progetti.

Sono state utilizzate diverse macro per facilitare la programmazione, ma il codice dovrebbe risultare comunque perfettamente comprensibile, data soprattutto l'abbondanza di commenti presenti. Quando si utilizzano delle macro, occorre tenere presente che il codice della macro viene effettivamente sostituito dal compilatore, quindi una macro di 10 istruzioni ripetuta 10 volte occupa 100 word di programma. Anche i salti con offset ($+x, $-x) devono essere calcolati seguendo questa considerazione.

Si procedera' ora ad una breve analisi della varie sezioni.

I listati del software, in assembler e in C.

Sezione main:
Questa sezione si occupa di avviare il sistema e di restare in attesa fino all'attivazione dell'interrupt del timer1. Per prima cosa viene dichiarata la configurazione del MCU,tramite la dichiarazione __CONFIG 0x2150. In questo caso, sono state disattivate le protezioni del codice e della memoria, ed il watchdog, ed e' stato abilitato l'oscillatore interno.
Vengono quindi dichiarati alcuni dei files da includere nel progetto: variabili, costanti, headers e macro.
All'indirizzo 4h viene creato il vettore interrupt.
La prima operazione che viene eseguita e' il PUSH, ovvero il salvataggio dei registri W, program counter,STATUS e FSR. L'istruzione PUSH e' una macro che salva questi registri su altrettante copie in RAM. Questo e' uno dei casi in cui puo' tornare utile il fatto che gli ultimi 16 byte di RAM siano indipendenti dal banco. In caso contrario sarebbe decisamente piu' difficile salvare lo stato dei registri senza modificarne il contenuto.
Dopo aver eseguito il PUSH, viene controllato il flag relativo al timer1. Dato che nel programma e' stato utilizzato solo questo interrupt, sarebbe possibile evitare il controllo. E' comunque consigliabile eseguirlo, dato che il programma potrebbe essere finito qui per gravi errori (del programmatore, il micro esegue solo quello che gli viene detto).
Se il flag di interrupt del timer1 e' attivo, viene riavviato il timer. In questo caso, non essendoci bisogno di temporizzazioni stringenti, e' sufficiente ricaricare i valori nei registri del timer, TMR1H e TMR1L (se fosse necessario ottenere tempi piu' precisi, sarebbe possibile sommare il contenuto dei registri del timer al valore iniziale, tenendo cosi' conto della decina di clock persi per la gestione dell'interrupt).
Si reimposta quindi l'interrupt del timer, cancellando il flag di evento e settando il flag di abilitazione, e quindi si attiva il flag di stato del timer.
Per motivi di comodita', al posto di utilizzare un altro timer effettivo, si e' preferito utilizzare come timer di movimento dei led un divisore del timer principale. Questo divisore, che e' stato chiamato timer2, viene decrementato ad ogni ciclo del timer1, finche' non avviene un passaggio per lo zero. A questo punto, viene reimpostato il divisore e attivato il flag di stato corrispondente.
Infine si esce dal vettore interrupt eseguendo l'operazione inversa del PUSH, il POP.
Dopo il vettore interrupt, e' presente il codice che il micro esegue effettivamente una volta acceso. Per prima cosa viene quindi eseguita l'inizializzazione. I pin necessari all'applicazione vengono impostati come ingressi o uscite (all'avvio del sistema, tutti i pin sono settati per default come ingressi, in modo da evitare situazioni non desiderate), agendo sui registri di controllo dei port, TRISA e TRISB (notare che questi registri si trovano nel banco 1 e non nel banco 0). Vengono quindi disattivati tutti i led (si lavora in logica negativa per motivi elettrici) e si inizializzano il timer1 e il timer2.
A questo punto possono essere abilitati gli interrupt ed il programma rimane in loop in attesa del timer1.
Quando il timer1 e' attivo, vengono eseguiti i controlli sulla tastiera e, se e' abilitata l'esecuzione (pulsante "start") sul PWM del programma5 e sul timer2.
Se il timer2 e' attivo, si esegue la sub di gestione dei programmi, che abilita il programma da eseguire controllando lo stato del pulsante "selezione", e si esegue il programma dei led abilitato.
Infine si torna a inizio ciclo, nuovamente in attesa del timer1.
Prima della fine del programma, decretata dall'istruzione END, si dichiarano gli altri files da includere nel progetto, quelli contenenti le sub delle varie sezioni.

Gestione programmi
Questa sezione e' composta da una routine, che si occupa di controllare l'esecuzione dei programmi di gestione led, e da una piccola sezione che gestisce il tasto "start".
Per prima cosa, viene verificato se e' avvenuto un cambio di stato del pulsante "selezione". In caso negativo, il programma abilitera' semplicemente l'esecuzione del programma attivo, settando il relativo flag del byte di controllo.
In caso affermativo, viene invece attivato il programma successivo a quello attivo, e sono inizializzate le variabili relative.

Programmi di gestione led
Tutti questi programmi si occupano della movimentazione dei led. A tal fine utilizzano un byte su cui effettuano gli spostamenti, demandando alla sub di scrittura sul port il compito di modificare le uscite corrette.

Lettura pulsanti
La sub di lettura dei pulsanti utilizza 2 byte come buffer antirimbalzo. Il buffer del tasto viene incrementato o decrementato a seconda che il pulsante sia premuto o meno. Quando il buffer arriva al valore di soglia il pulsante viene considerato premuto, mentre quando arriva a zero viene considerato non premuto. Di conseguenza vengono impostati i flag di stato dei pulsanti.
Il buffer e' dimensionato a 64 cicli del timer1. Se il pulsante rimane premuto piu' a lungo, viene considerato come se non fosse ancora stato rilasciato. Di conseguenza, per ottenere ad esempio il cambio dal programma 1 al programma 4, occorre premere e rilasciare (per almeno 64 cicli ogni volta) il pulsante per 3 volte.

Timer
Questa sezione si occupa dell'inizializzazione dei timer. Timer1 viene abilitato con prescaler a 1, e caricato con il valore desiderato. Viene quindi attivato l'interrupt relativo e azzerato il divisore timer2. Si ricorda che per caricare un valore nel timer1, occorre caricare prima il MSB e poi l'LSB, poiche' il contatore carica la word in seguito al caricamento del LSB (se si tenta di caricare i dati al contrario, si carichera' nel timer1 un valore corrispondente al LSB desiderato, lasciando l'MSB nello stato precedente - ignoto).
Per motivi progettuali, legati soprattutto al PWM, timer1 e' stato impostato a 1ms, mentre timer2 e' pari a 100ms. In queste condizioni l'effetto scia e' ben visibile ed il movimento dei led abbastanza fluido.
Torna all'indice




Impaginazione by KCS'81