Meglio il C o l’assembler?

  • by

Questa è una domanda che spesso si pone chi lavora con i controllori e scrive firmware. Vorremmo avere una risposta universalmente applicabile, ma ogni volta che andiamo in una direzione ci torna il dubbio che forse l’altra scelta sarebbe stata la migliore.
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

  • 1GL (first generation language) è in effetti il codice macchina. Si tratta della rappresentazione binaria che il processore utilizza ed esegue. Nella programmazione dei controllori lo vediamo leggendo i files .hex che trasferiamo sul controllore. Il programma è costituito da una serie di caratteri binari, come descritto nel datasheet del controllore.
  • 2GL (second generation language) di fatto è l’assembler (o assembly) è poco più che una traduzione in codice mnemonico dei comandi binari.
  • 3GL (third generation language) linguaggi di più alto livello solitamente di tipo procedurale (per esempio C o Java). Il compilatore traduce in codice macchina, effettuando operazioni più complesse della semplice traduzione letterale come avviene per i 2GL.
  • 4GL (fourth generation language) si tratta di linguaggi dalla sintassi che tende ad avvicinarsi al linguaggio naturale (SQL ne è un esempio). Lo cito solo per completezza
  • 5GL (fifth generation language). Sono linguaggi di programmazione che utilizzano ambienti grafici o visuali per interfacciarsi col programmatore. Solitamente si basano comunque su linguaggi 3GL o 4GL.

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:

  • Variabili
  • Tipi di dati complessi
  • Iterazioni (o cicli)
  • Scelta
  • Procedure

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:

  1. Analisi del codice in termini di “albero delle chiamate”. Elenco di tutte le funzioni con le relative dipendenze (chi chiama chi). Utile per vedere le funzioni che convivono e che quindi non possono condividere aree di memoria
  2. Catalogazione di tutti gli oggetti (funzioni) in termini di dimensione del codice e richiesta di memoria.
  3. Analisi dei parametri di input e resultcode.
  4. Creazione della mappa di memoria. Solitamente viene suddivisa in segmenti logici, dedicati a diverse tipologie di esigenza (variabili, parametri, tipi di dati pi lunghi del byte, …)
  5. In caso di limitazioni del compilatore effettua la gestione per mascherare le limitazioni. Ad esempio i processori di fascia bassa della Microchip hanno una limitazione sulla profondità di chiamata delle funzioni (max 2 livelli) il compilatore deve aggirare il problema. Il compilatore picc di HI-TECH si costruisce una jump table che usa per mascherare call/return con coppie di goto.

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

  • è un linguaggio più comodo. Offre una sintassi e una gestione procedurale che ne agevola la scrittura e la lettura da parte dell’uomo
  • è pressoché indipendente dall’hardware sottostante. Attenzione: questo non significhi che TUTTI i programmi scritti in C sono effettivamente indipendenti dall’hardware. E nel caso di firmware per microcontrollori questo lo si vede spesso ed è una dell considerazioni principali nella scelta del linguaggio. A cosa mi riferisco? L’istruzione RB3 = 1; è un comando C, ma se serve per accendere un LED collegato al piedino 3 della porta B del PIC, questa è una conoscenza hardware necessaria per il funzionamento del programma. Quando programmiamo un microcontrollore non vale l’analogia con il computer. In questo caso, infatti, solitamente il programma C interagisce con il sistema operativo e difficilmente abbiamo la necessità di utilizzare direttamente componenti Hardware.
  • La traduzione in codice macchina perde in efficienza
  • Generalmente la scrittura di codice richiede meno tempo

Assembler

  • permette accesso diretto all’hardware
  • è possibile gestire con precisione i tempi di esecuzione di ogni istruzione
  • la programmazione richiede una buona conoscenza del processore su cui il codice viene eseguito
  • richiede molta attenzione e precisione nella definizione delle strutture.

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.

Leave a Reply