In questa ultima puntata facciamo alcune considerazioni sull’uso di linguaggi ad alto livello nel campo della programmazione dei microcontrollori e facciamo conoscenza con il Motorola 68HC11A1
Negli scorsi articoli abbiamo imparato le problematiche della programmazione in C di un microcontrollore facendo pratica con il demo dell’Hitech-C per PIC. Abbiamo preso in esame molti aspetti e visto molti esempi e adesso è venuto il momento di tirare le somme e fare alcuni commenti.Al di là del compilatore usato abbiamo visto che l’uso del C semplifica notevolmente l’apprendimento e consente di focalizzare l’attenzione su come risolvere i problemi, sorvolando, almeno in prima battuta, i problemi relativi all’implementazione pratica. Per piccoli progetti questo aspetto ha un rilievo solamente didattico, ma nel caso di grandi progetti, magari sviluppati da team di programmatori, tecnici ed ingegneri diventa rilevante. Abbiamo visto che con il PIC non si possono sviluppare progetti complessi in C, ma con tanti altri microcontrollori al limite si sviluppa un certo insieme di funzioni critiche in assembly ma il resto in C.
Un’altra cosa importante è che scrivendo in linguaggi ad alto livello si facilita una eventuale manutenzione successiva del codice e una certa riusabilità. Il problema della manutenzione e del debugging è molto importante anche per progetti hobbistici e questa serie di articoli è stata pensata proprio per questo target.
Nel campo della programmazione dei microcontrollori linguaggi ad alto livello vuol dire C, prossimamente C++ e Java ma comunque non basic o Pascal, anche se esistono compilatori per tali linguaggi per la maggior parte dei microcontrollori. La grande diffusione del C è dovuta anche al riutilizzo quasi immediato di una immensità di pezzi di codice di pubblico dominio che riduce notevolmente i tempi di sviluppo. Per le aziende il C può talvolta essere l’unico linguaggio usato: esistono sistemi operativi multitasking sia di pubblico dominio sia commerciali per i principali microcontrollori e basta sviluppare una libreria di base per interfacciarsi all’hardware, dopodiché ogni progetto sarà realizzato chiamando le funzioni di tale libreria. Torno a sottolineare che questo comunque non esenta dal dover conoscere l’architettura ed il linguaggio assembly: bisogna avere la cognizione delle capacità e dei limiti del sistema su cui si sta lavorando.
Il problema principale che si sente programmando in C è la mancanza di un tipo di dato bit, alcuni compilatori, come l’MPLAB-C e l’Hitech-C lo forniscono, ma non è standard e quindi pochi programmatori lo usano. Un altro problema sono le interruzioni: è raro che un compilatore riesca a capire quando serve e quando no salvare il contesto (tutti o alcuni registri) e la conseguenza sono ISR lente e voluminose e addirittura la mancata gestione di interrupt quando i tempi di esecuzione delle routine di servizio non sono compatibili con le specifiche.
La maggior parte dei compilatori attuali generano codice ottimizzato, ma in realtà non tutti sono "intelligenti": capita spesso analizzando il sorgente assembly da loro generato di vedere una serie di istruzioni del tipo:
muovi indirzzo1 nel registro1
muovi #1 in indirizzo puntato da registro 1
muovi indirzzo1 nel registro1
muovi #2 in indirizzo puntato da registro 1
Notate la ridondanza inutile della prima e seconda muovi: queste situazioni o altre di simili sono quelle che inducono un programmatore assembly ad abbandonare il C.
Sapere come si comporta il compilatore che si usa è di grandissima importanza nel campo della programmazione in generale, figuratevi in quella dei microcontrollori! Per questo scopo è far generare al compilatore il listing del programma, ossia un file (generalmente con estensione .LST) che contiene per ogni riga di C il codice assembly generato.
Ecco il listing di un semplice programma generato dal demo dell’MPLAB-C:
#include <16f84.h>
#ifndef _16C84A_H
/*
PIC16C84A Standard Header File, Version 1.00
(c) Copyright 1996 Microchip Technology, Inc., Byte Craft Limited
RAM locations reserved for temporary variables: 0x0C - 0x10
*/
#pragma option +l;
#endif
void main() {
0005 1683 BSF 03,5 TRISB=0;
0006 0186 CLRF 06
0007 0185 CLRF 05 TRISA=0;
0008 1283 BCF 03,5 PORTB=0;
0009 0186 CLRF 06
while(1) {
000A 3001 MOVLW 01h
000B 1283 BCF 03,5
000C 0086 MOVWF 06
000D PORTB=1;
000D 0185 CLRF 05 PORTA=0;
000E 0186 CLRF 06 PORTB=0;
000F 0085 MOVWF 05 PORTA=1;
0010 280A GOTO 000Ah }
0011 0008 RETURN }
Senza addentrarci nell’assembly del PIC notiamo che le BSF 3,5 o BCF 3,5 , che servono per impostare il banco di memoria corretto (bit 5 del registro 3, cioè dello status register), precedono le istruzioni di impostazione di un registro (ricordo che nel PIC sia la RAM sia i registri sono divisi in 2 banchi). L’MPLAB-C è abbastanza intelligente da non settare il bit nuovamente per la PORTA=0 dopo la PORTB=1, ma non abbastanza per rendersi conto che è una invariante nel ciclo è può essere spostata fuori. Comunque è un errore veniale, anche se rende il programma scritto in C un bel 15% più lento di uno ottimizzato in assembly.
Fra un po’ vedremo come se la cava il gcc, il compilatore freeware della GNU, con i patch per creare programmi per 68HC11, ma prima facciamo conoscenza con questo microcontrollore.
Il 68HC11A1FNE’ il microcontrollore della Motorola più adatto agli hobbisti, grazie alle sue numerose periferiche integrate. E’ disponibile nel formato PLCC 52 piedini, quindi in tecnologia SMD e questo rende indispensabile farsi un circuito stampato per poter usarlo: in questo articolo prenderò come riferimento la scheda presentata dalla rivista Fare Elettronica a partire dal numero 118 e non ci saranno schemi elettrici.
Il 68HC11 è un microcontrollore a 16 bit basato su un nucleo molto convenzionale CISC con un registro accumulatore e due registri indice. Il set di istruzioni è una evoluzione a 16 bit di quello del 6500, ha istruzioni di moltiplicazione e di divisione, supporta linguaggi come il C e sistemi operativi multitasking.
La versione qui presa in esame dispone delle seguenti periferiche: 512 byte di memoria EEPROM (programmi e dati), 256 byte di memoria SRAM (programmi e dati), 8kb di memoria ROM con monitor e assemblatore, 5 porte di I/O (A: 4 pin O, 3 I, 1I/O, B: 8 pin O, C: 8 pin I/O, D: porta seriale sincrona e asincrona, E: 8 pin di ingresso digitali o analogici con risoluzione di 8 bit), 1 timer 16 bit, 1 contatore di impulsi esterni ad 8 bit, 1 watchdog timer.
L’HC11 può funzionare sia in modalità estesa, cioè utilizzando le porte B e C come bus dati ed indirizzi, sia in modalità single-chip eseguendo le istruzioni contenute nelle memorie interne. Si programma tramite la porta seriale asincrona interfacciandola alla RS-232 del PC attraverso un integrato convertitore di tensione Max232.
Il GCC per HC11Il 68HC11 si può programmare in assembly, BASIC, Forth, C, C++, Objective C e Pascal: c’è quindi solo l’imbarazzo della scelta. Come già scritto esiste un patch per il gcc che fa generare codice assembly 68xx a questo famoso compilatore e quindi si può sviluppare anche sotto Linux o qualsiasi altro sistema su cui giri il gcc. Ecco come risulta su 68HC11 il programma per far lampeggiare un led:
void aspetta(int n);
main()
{
char *registri;
registri=(char*)0x1000; /* inizio registri */
while(1)
{
registri[4]=0xff; /* porta B (solo output) */
aspetta(10000);
registri[4]=0;
aspetta(10000);
}
}
void aspetta(int n)
{
int i;
for (i=0;i<n;i++);
}
Questa volta abbiamo a che fare con un C ANSI (o quasi…) e quindi non ci sono limitazioni o bug da tenere a memoria.
Come si vede anche nel 68HC11 l’I/O è memory-mapped e all’indirizzo 0x1000 è il primo dei registri per interfacciarsi alle periferiche integrate.
I lettori più attenti si saranno accorti certamente di una importante differenza con l’analogo programma per PIC: manca la parte di inizializzazione dell’hardware ed il loop infinito finale. Questo dipende dal compilatore usato: mentre con l’Hitech-C il main corrispondeva alla prima istruzione eseguita dal PIC, il gcc è stato fatto per interfacciarsi ad un sistema operativo. Per produrre codice direttamente scaricabile sulla EEPROM il gcc si appoggia ad un modulo oggetto precompilato (crt0.o) che è in pratica un mini-sistema operativo: se si vuole generare un file scaricabile sulla EEPROM bisogna quindi linkare al modulo oggetto del nostro programma il modulo crt0.o.
Il gcc è il compilatore che si usa sotto Linux e quello su cui è basato il DJGPP (c’è anche per AmigaDOS, Mac, workstation Alpha, MIPS, Sparc) e può funzionare sia come compilatore normale sia come cross-compiler sia come canadian-compiler. Questa ultima espressione significa che si può generare con il gcc per Linux una versione del gcc che funzioni sotto DJGPP/DOS ma che generi codice 68hc11. Il gcc tra l’altro è alla base del sistema commerciale di cross-compilatori per il mercato dei sistemi embedded GNUPro Toolkit 97 della Cygnus Solutions.
Chi ha avuto occasione di vederne i sorgenti si sarà accorto di una selva di directory target; tra queste vi sono già quelle per l’H8, un microcontrollore della Hitachi, ma si può aggiungere altri target fornendo le appropriate informazioni al compilatore.
Questa estrema configurabilità è dovuta alla particolare architettura del gcc: invece di compilare da C a codice macchina o assembly direttamente genera internamente un intermedio universale (RTL: Register Transfer Language) che poi viene tradotto. In questo modo cambiando l’ultima parte della compilazione si possono supportare diverse architetture.
Le varie directory target contengono dei files che implementano il traduttore da formato intermedio ad assembly, le librerie basilari per il compilatore (vale a dire quelle che servono per supportare il C, come divisioni, moltiplicazioni, modulo ed altre cose) e le librerie standard (ovviamente queste dipendono non solo dalla CPU ma anche dal sistema operativo sul quale dovrà girare il programma. Nel caso sia configurato come cross-compilatore per HC11 il gcc ha quasi tutto quello che serve per generare codice C, C++ e addirittura Objective C, tranne le routine di gestione della memoria (per il new ed il delete del C++). Il modulo oggetto corrispondente al programma è reso dipendente dal crt0.o, detto modulo di startup ed esistente per tutti i compilatori: il Borland C, per esempio ne ha varie versioni che servono per interfacciarsi in vari modi alla memoria (near, far, huge…).
Prima di compilare il nostro primo programma dobbiamo quindi adattare il file crt0.s alle nostre necessità e dopo assemblarlo per generare il crt0.o. Vediamo il crt.s dello xgcc: il gcc per DOS che genera codice 68hc11.
Ci sono due file che vengono inclusi per fare il crt0.s: crt0base.s e crt0boot.s. Come si evince dalla estensione .s sono scritti in linguaggio assembly, comunque non spaventatevi perché non mi dilungherò sul set di istruzioni del 68hc11. Questa è la prima parte del file crt0base.s:
;-----------------------------------------
; CRT0BASE.S
; Basic definitions for the compiled code.
; Registers are located at the end of the
; first page (=256 Bytes) of the M68HC11
;-----------------------------------------
.module crt0base.s
.area DIRECT (ABS,PAG)
.org 0
Queste direttive valgono per l’assemblatore e per il linker forniti con l’xgcc, comunque si trovano loro equivalenti su tutti gli assemblatori e linker. La direttiva .area specifica l’inizio di una area di memoria in cui mettere i dati o il codice che segue. L’area di memoria può essere assoluta e cioè corrispondere direttamente ad indirizzi reali del sistema da programmare oppure rilocabile, cioè nel sorgente non è specificato l’indirizzo fisico in cui collocare l’area e alla fine il programmatore dovrà indicarlo manualmente al linker. Grazie a questa direttiva si può specificare per esempio di mettere i dati in RAM ed il codice in EEPROM. In questo caso si tratta di un’area di dati riscrivibili da mettere su RAM all’indirizzo assoluto specificato dalla seguente direttiva .org (all’indirizzo 0, infatti, inizia la RAM interna del 68HC11A1).
Seguono le definizioni degli pseudo-registri, ognuno richiedente uno spazio di 2 byte (.blkb 2). Gli pseudoregistri sono una particolarità del gcc per hc11: poiché infatti il gcc è stato sviluppato pensando a CPU con molti registri mentre il 68HC11 ne ha solo 3 la soluzione è stata di usare alcune locazioni fisse di RAM come registri. Questo porta innegabilmente ad appesantire il codice (sia come velocità sia come lunghezza), ma se non vi va bene potete tentare voi di fare un compilatore C++ partendo da zero! Comunque il gcc tutto sommato è un buon compilatore e talvolta anche in assembly è necessario usare delle variabili temporanee per sopperire alla penuria di registri.
;-----------------------------------------
; required gcclib code
;-----------------------------------------
;
; signed divide: assumes numerator on stack and denominator on stack + 2
; returns quotient in D
;
.area _CODE
Da qui inizia la parte di codice, come si vede dalla direttiva .area, ed in particolare si tratta del codice necessario al gcc per eseguire le operazioni di modulo, divisione e moltiplicazione a 16 bit, dal momento che il 68HC11 esegue queste operazioni solo a 8 bit. Notate come gli argomenti siano passati tramite lo stack e non tramite registri come per il PIC.
L’area CODE di solito si mette su memoria non volatile (nel nostro caso EEPROM).
Passiamo ora al file crt0boot.s, che come dice il nome si occupa della parte di inizializzazione (è in questo file, quindi che è presente una specie di mini sistema operativo).
;-----------------------------------------
; Stack and Jump Area
;-----------------------------------------
.module crt0boot.s
; We could use a fixed and absolut area for the stack
; .area STACK (ABS)
; .org 0x0100
; But it would be better to put it in the _BSS area.
; This allows a more flexible adaption to other hardware.
.area _BSS
__stack_end::
.blkb 96 ; CHANGE THIS VALUE, IF THERE IS MORE MEMORY AVAILABLE
__stack_begin::
Tradizionalmente l’area BSS è destinata ai dati da mettere su memoria RAM, per esempio quelli dello stack.
Segue la direttiva per assegnare lo spazio per i vettori delle interruzioni e poi il codice di inzializzazione (di nuovo su area CODE), di cui riporto solo una piccola parte:
lds #__stack_begin-1 ; initialize stack pointer
jsr __build_irt_table ; build the interrupt table
ldx #0 ; clear the NULL pointer
stx *_os_null_ptr
clr _os_sei_cnt ; clear interrupt disable counter
cli
jsr _main ; main()
_exit::;
exit:: bra exit
___main::
rts ; return from function
Vengono inizializzate varie cose, tra cui lo stack-pointer e i vettori delle interruzioni, e poi viene chiamato il main come una normale funzione, dopodiché l’esecuzione prosegue con il ciclo infinito bra exit (branch, diramazione, corrisponde ad un GOTO). Notate il carattere _(underscore) davanti al main: in assembly i nomi C vengono sempre preceduti da questo carattere.
Il resto del file è abbastanza difficile da capire, in particolare la parte di inizializzazione dei vettori di interruzione, comunque l’importante è sapere cosa fa il modulo di startup e averne una conoscenza sufficiente a personalizzarlo.
Adesso che sappiamo come è organizzato il gcc vediamo un po’ di valutare la sua efficienza.
Ecco un pezzo del risultato di un cc1 primo.c –O3 –fomit-frame-pointer:
;;;-----------------------------------------
;;; Start MC6811 gcc assembly output
;;; gcc compiler compiled on TBD
;;; OPTIONS: -mlong_branch optimize inline-functions
;;; OPTIONS: peephole !signed-char
;;; Source: primo.c
;;; Destination: primo.s
;;; Compiled: Thu Feb 12 21:39:18 1998
;;; (META)compiled by GNU C version 2.6.3.
;;;-----------------------------------------
.module primo.c
; extern _aspetta
.area _CODE
.globl _main
_main:
;;;-----------------------------------------
;;; PROLOGUE for main
;;;-----------------------------------------
pshy ; Save stack frame
tsy ; Set current stack frame
ldx *ZD5
pshx ; pushed register *ZD5
;;;END PROLOGUE
jsr ___main ; CALL: (VOIDmode) ___main (0 bytes)
ldd #4096
std *ZD5 ; movhi: #4096 -> *ZD5
L9:
ldab #255
ldx *ZD5
stab 4,x ; movqi: #255 -> 4,x
ldd #10000
std *ZD0 ; movhi: #10000 -> *ZD0
jsr _aspetta ; CALL: (VOIDmode) _aspetta (0 bytes)
ldab #0
ldx *ZD5
stab 4,x ; movqi: #0 -> 4,x
ldd #10000
std *ZD0 ; movhi: #10000 -> *ZD0
jsr _aspetta ; CALL: (VOIDmode) _aspetta (0 bytes)
jmp L9
;;;EPILOGUE
pulx ; Pulling register *ZD5
stx *ZD5
puly ; Restore stack frame
rts ; return from function
;;;-----------------------------------------
;;; END EPILOGUE for main
;;;-----------------------------------------
.globl _aspetta
Anche se le opzioni hanno impostato la massima ottimizzazione il risultato non è stato molto brillante, anzi direi piuttosto deludente. Il problema come previsto è nella soluzione dei registri fittizi in RAM. Per fortuna, comunque, esistono molti altri compilatori C per HC11 e l’attrattiva del gcc è che se si implementassero le funzioni per il new ed il delete si potrebbe programmare l’HC11 in C++.
La conclusione che possiamo trarre dall’esame dei codici assembly generati dai 2 compilatori è che i limiti e le debolezze di un microcontrollore sono amplificati notevolmente dall’uso di un linguaggio ad alto livello e che questo rende di fatto impossibile sviluppare certi programmi in un linguaggio diverso dall'’assembly.
CONCLUSIONINel primo articolo si accennava alle novità del prossimo futuro: Java e Windows CE. I microcontrollori Java based devono ancora uscire (forse entro fine 1998), mentre Windows CE è già alla versione 2, ma ad oggi un palmare con Windows CE costa ancora troppo. Tuttavia consiglio caldamente a tutti coloro che sono interessati allo sviluppo di sistemi embedded di tenere sotto osservazione queste due tecnologie.
Nel frattempo dovrebbero uscire dei nuovi PIC e nuovi microcontrollori della Hitachi e della Nec con 50MIPS a 32 bit con "solo" un centinaio di pin. Nel campo del software si aspettano compilatori C++ e tools RAD, che dovrebbero snellire i tempi di sviluppo e rendere più semplice il debugging, però almeno finche non ci sarà la rivoluzione Java assembly e C la faranno da padroni sia per la programmazione hobbistica sia industriale.
Bene, siamo arrivati alla fine di questa breve ma intensa serie sulla programmazione dei microcontrollori. Abbiamo imparato cos’è e come si usa un microcontrollore e abbiamo trattato alcune tecniche di uso comune nel campo dei sistemi embedded. Ho cercato di mettere in luce differenze e similitudini con la normale programmazione per PC e spero che molte cose dette siano state interessanti anche per i lettori non direttamente interessati alla programmazione su microcontrollori. Non mi resta che salutarvi ed augurarvi buona programmazione!