Introduzione
Uno dei progetti che stiamo attualmente seguendo ha richiesto l’utilizzo di tecnologie più a basso livello rispetto alla programmazione nei linguaggi più comuni, stiamo parlando nello specifico della programmazione in Assembly.
Assembly (o linguaggio assemblativo) è uno dei primissimi linguaggi di programmazione, pochi livelli sopra gli 1 e gli 0, e molto più complicato rispetto ai linguaggi moderni. Molti dispositivi con capacità limitate contengono ancora codice scritto in Assembly, in quanto rimane uno dei linguaggi più efficienti a livello di velocità di esecuzione e di memoria occupata.
Il dispositivo che abbiamo utilizzato è un piccolissimo chip con solo 8kB di disco, 256B di RAM e dotato di una A.L.U. a 8-bit.
Nonostante le caratteristiche parecchio limitate, è comunque in grado di:
- controllare dispositivi esterni tramite pin,
- effettuare semplici operazioni logiche e matematiche come somma e sottrazione,
- interfacciarsi con il mondo esterno attraverso periferiche interne come un convertitore Analogico/Digitale.
Motivazioni tecniche
Abbiamo fatto queste scelte seguendo una filosofia Design-To-Cost, riducendo al minimo i costi di produzione, che diventano significativi quando il numero di pezzi da produrre è considerevole, utilizzando solo lo stretto necessario. Nel nostro caso un dispositivo con le caratteristiche sopraelencate era tutto quello di cui avevamo bisogno.
La scelta di utilizzare Assembly è stata fatta per venire incontro alle capacità limitate dell’Hardware. La casa produttrice del microchip da la possibilità di scrivere in C ma questo comporterebbe un aumento della memoria utilizzata e meno controllo sul singolo bit. Data la complessità del codice necessario, questa non era una strada percorribile.
Ragionamenti simili vengono spesso fatti quando si prevede la produzione in grandi quantità. Va anche detto che molti dei dispositivi elettronici utilizzati nel quotidiano contengono chip programmati in modo simile a quello che abbiamo fatto in questo progetto.
Sviluppo Software in Assembly
Il software sviluppato si può dividere in due macro aree, Software funzionale e Software di sicurezza.
Software Funzionale:
Quest’area del programma contiene tutte le procedure necessarie per il corretto funzionamento del prodotto. Questo comprende sia codice riguardante l’interfaccia utente (bottoni, led…) sia il ciclo di funzionamento del dispositivo stesso.
È da sottolineare il fatto che questa è anche la parte più a stretto contatto con l’hardware e l’elettronica del dispositivo. Tramite i pin e gli strumenti forniti dal chip il programma svolge diverse funzioni come modificare il proprio comportamento al click di un bottone, attivare/disattivare switch di alimentazione e persino accorgersi di variazioni nell’ambiente esterno e comportarsi di conseguenza.
Software Di Sicurezza:
La seconda parte del programma si occupa di controllare la salute dei componenti fisici del micro-controllore. È necessario effettuare controlli periodici a intervalli frequenti in modo da accorgersi di eventuali guasti e interrompere l’esecuzione del programma.
Abbiamo seguito le indicazioni previste dal UL/CSA 60730 safety standard (Annex R) che definisce i test necessari e le condizioni di guasto. Alcuni esempi di test sono:
- Test della CPU. Eseguendo le varie operazioni supportate e verificandone i risultati possiamo rilevare eventuali guasti,
- Test della ROM. Attraverso l’esecuzione di un algoritmo CRC otterremo un risultato inaspettato solo se una cella nella ROM è guasta o se il codice è stato modificato,
- Test dei Clock. Utilizzando due clock a velocità diverse possiamo usarli per verificarsi a vicenda,
- Test della RAM e dei Registri. Impostando a 0 e 1 alternati ogni cella della RAM e ogni Registro e invertendoli possiamo accorgerci di eventuali bit “bloccati”. A seguire un esempio di codice:
Tecniche programmazione Assembly
A seguire elenchiamo alcune tecniche di sviluppo che abbiamo utilizzato per risolvere alcuni problemi legati al progetto.
Mancanza di istruzioni di branching logico (EQU, NEQ …):
Come forse avrete notato dall’esempio di codice mostrato sopra, la ALU di questo micro-controllore non supporta EQU e NEQ (Equal e Not Equal). È quindi necessario trovare modi alternativi per confrontare due valori.
Utilizzando operazioni come lo xor o la sottrazione possiamo capire se una variabile è uguale/maggiore/minore ad un altra analizzando il risultato e eventuali flag di riporto. Benché questo approccio richiede due operazioni al posto di una è l’unico modo che abbiamo per confrontare due variabili.
Jump Table:
Una Jump Table è una tecnica di programmazione Assembly utilizzata in maniera simile ad uno Switch dei linguaggi moderni e utilizzata spesso all’interno di questo progetto. Si basa sul concetto di Jump Relativo, una manipolazione manuale del flow del programma.
Posizionando una serie di operazioni (generalmente Jump a operazioni più complesse) ad una cella di memoria di distanza l’una dall’altra e sommando un valore intero N al Program Counter (contatore che indica la riga di codice che sta venendo eseguita al momento) possiamo spostarci alla N-esima operazione della tabella. Ci sono possibili problematiche da tenere in considerazione, come un possibile overflow del P.C. ma è una tecnica potente che permette di scrivere codice complesso in modo efficiente.
Ad esempio, la porzione di codice nell’esempio sopra viene raggiunto attraverso un Jump gestito da una Jump Table.
Macro:
Le macro sono porzioni di codice “riutilizzabile” in grado di ricevere variabili in input. A differenza delle procedure questo codice non è effettivamente presente nel programma. Invece viene “Copiato e Incollato” durante la compilazione del programma nel punto in cui la macro viene chiamata.
Anche se sono uno strumento molto potente le macro comportano dei rischi. Infatti se utilizzate senza le giuste precauzioni possono causare problemi di memoria che, in progetti sensibili come il nostro, possono risultare anche gravi.
Periferiche hardware utilizzate
In questo progetto abbiamo utilizzato diversi dispositivi hardware collegati al micro-controllore tra cui:
- Convertitore Analogico/Digitalale, per misurare valori analogici da sensori esterni
- Oscillatore in quarzo esterno, per verificare il corretto funzionamento dei clock interni
- EEPRom (64 Byte), utilizzata come memoria Read/Write per log o costanti di produzione
Tutti questi componenti sono controllati dal codice attraverso registri specifici e associati, se necessario, a specifici Pin.
Problematiche risolte
Ecco alcune problematiche che abbiamo affrontato e risolto durante lo sviluppo di questo software:
- Eseguire i controlli senza bloccare il corretto flow del ciclo Main.
Utilizzando la Jump Table e spezzando in parti i controlli possiamo eseguire i test per la sicurezza tenendo occupato il codice pochi millisecondi a ciclo.
- Garantire l’integrità del programma dopo eventuali scatti di Interrupt.
Utilizzando in modo corretto gli Stack di memoria e capendo i dettagli nel funzionamento degli Interrupt siamo riusciti a gestire eventi come il click di un bottone senza compromettere l’integrità del programma.
- Mantenere il tutto all interno degli 8KB di disco e 256B di memoria Ram.
Ottimizzando il codice e utilizzando tecniche di programmazione Assembly abbiamo completato lo sviluppo del codice mantenendo il tutto entro le dimensioni ridotte fornite dal microchip.
- Implementare precauzioni per eventuali comportamenti fuori controllo dati da sbalzi di tensione
Lavorando così a stretto contatto con l’elettronica, anche un piccolo sbalzo nella tensione può causare comportamenti imprevedibili. Implementando controlli per le parti più sensibili del programma, abbiamo impedito che questo tipo di anomalie possa causare errori critici.
- Program-Counter stack limitato
Il micro-controllore è dotato di uno stack del program counter di dimensioni abbastanza ridotte. Questo stack viene utilizzato per salvare il valore corrente del Program-Counter ogni volta che viene chiamata una funzione con return in modo che il programma possa riprendere da dove ha lasciato. Il fatto che sia di dimensioni limitate rende molto rischioso l’utilizzo di cicli e la chiamata a procedure annidate. Utilizzando le procedure in maniera attenta ed evitando chiamate innestate o ricorsive possiamo evitare problemi di Stack Overflow.
Conclusioni
Il linguaggio Assembly è senza ombra di dubbio un linguaggio complicato, pieno di particolarità non presenti in linguaggi moderni. Nonostante ciò rimane importante anche per la tecnologia moderna e per casi di sviluppo che richiedono un livello di controllo a basso livello e un’estrema razionalizzazione delle risorse.
Questo progetto per noi è stato sicuramente un esperienza diversa dal solito, essendo di natura molto differente dal Cloud IoT su cui generalmente lavoriamo. Nonostante ciò abbiamo accettato la sfida e ne abbiamo imparato molto risolvendo volta per volta i problemi che ci venivano presentati.