Linguaggio C per i microcontrollori - puntatori, tipi e operatori
In questa pagina 7
La parte 3 di ogni appello (circa 10 minuti) è fatta di domande sul linguaggio C usato per programmare i µC (Microcontrollori STM32 - clock, timer, ADC e codice HALI µC STM32 (core ARM Cortex-M) del corso si configurano con la libreria HAL: si riempiono strutture C (...InitTypeDef) con valori simbolici e si chiamano funzioni di libreria. Clock (STM32F334R8): oscillatore esterno HSE 8 MHz × PLL (MUL9) = SYSCLK 72 MHz (massimo); AHB ÷1, APB1 ÷2 (36 MHz), APB2 ÷1 (72 MHz); clock dell'ADC = clock del bus ÷ divisore (÷6 → 12 MHz). Timer in PWM: $f=f{clk}/((PSC+1)(Period+1))$, Pulse = duty $\times$ conteggi. Toggle: onda quadra a metà della frequenza di match. ADC: SAR a 12 bit, conversioni injected avviate da un trigger del timer, lettura con interruzione.Microcontrollori STM32 - clock, timer, ADC e codice HAL →): leggere un frammento e dire che cosa stampa o che cosa fa. Gli errori tipici sono sempre gli stessi: dimensione dei tipi, incremento prefissooperatore scritto prima della variabile: ++i incrementa e poi restituisce il nuovo valore e postfissooperatore scritto dopo la variabile: i++ restituisce il valore prima dell'incremento, aritmetica dei puntatoriregola per cui sommare 1 a un puntatore lo fa avanzare di un elemento del tipo puntato, macrosostituzione di testo eseguita dal preprocessore prima della compilazione, promozioni di tipo.
Dimensioni dei tipi (processore a 32 bit)
| Tipo | Byte | Note |
|---|---|---|
char, int8_t, uint8_t |
1 | |
short, int16_t, uint16_t |
2 | |
int, long, int32_t, uint32_t |
4 | |
long long, int64_t |
8 | |
float |
4 | virgola mobile in singola precisione |
| puntatore (qualunque) | 4 | l'ampiezza dell'indirizzo è la parola della macchina: 32 bit = 4 byte |
{{sizeof|operatore che restituisce la dimensione in byte di un tipo o di una variabile}} restituisce byte. Su un processore a 32 bit int *pa; sizeof(pa) vale 4 (non 32: 32 sono i bit). I tipi a larghezza esatta (uint16_t, int32_t della libreria {{stdint.h|libreria C con i tipi a larghezza esatta, come uint16_t e int32_t}}) sono quelli che si usano per i dati a virgola fissa (Realizzare un filtro o un regolatore a virgola fissaRealizzare in C un filtro a virgola fissa vuol dire scegliere per ogni dato il formato (coefficienti $<1$ in 0.16 U, coefficienti $>1$ in 1.15, 4.12, 5.11 S...), moltiplicare con aritmetica a 32 bit ($16\times16\to32$) e riportare il prodotto al formato di arrivo con uno shift a destrascorrimento dei bit verso destra: equivale a dividere per una potenza di 2; per sommare due prodotti con scale diverse si fa lo shift prima di sommare. Con $a+b=2^{16}$ il filtro $y=\big(a,y_{k-1}+b,(u\ll4)\big)\gg16$ non va in overflow in un uint32_t. Nel codice il formato di ogni variabile va dichiarato in un commento; la funzione $\sin x\approx x-\frac{x^3}6+\frac{x^5}{120}$ mostra come si approssima una funzione con polinomi a coefficienti in virgola fissa.Realizzare un filtro o un regolatore a virgola fissa →).
Puntatori
Un puntatore contiene l'indirizzo di una variabile: int *pa = &a; ( = indirizzo di; *pa = valore puntato). Un puntatorevariabile che contiene l'indirizzo di un'altra variabile a un tipo di byte, incrementato di 1, avanza di byte (aritmetica dei puntatori: pa+1 punta alla locazione successiva dello stesso tipo).
Esempio 1 (tema d'esame del 3 settembre 2020, processore a 32 bit):
int a = 10; int *pa = &a;
printf("Il valore puntato è: %d", *pa); // 10
printf("La dimensione del puntatore è: %ld", sizeof(pa)); // 4Risposta: 10, 4.
Esempio 2 (appello gennaio 2021 e 2022):
short int a = 10; short int *pa = &a;a. *pa vale 10. b. sizeof(pa) = 4 (è un puntatore). c. sizeof(a) = 2 (short). d. printf("%d", (*pa)++); stampa 10: l'operatore postfisso restituisce il valore prima di incrementare; dopo l'istruzione a vale 11. e. printf("%d", *(pa++)); stampa 11 (il valore attuale di a) e poi sposta pa di sizeof(short)=2 byte, quindi pa non punta più ad a.
Incremento prefisso e postfisso
i++(postfisso): l'espressione vale il valore di prima, poiiaumenta.++i(prefisso):iaumenta, poi l'espressione vale il valore nuovo. Esempio 3 (appello febbraio 2022,doSomething(30)):
int doSomething(int i) { return (i++); } // restituisce 30 (valore prima di incrementare)
int main(void) { int i = doSomething(30); printf("%d", --i); } // --i = 29La funzione restituisce 30 (l'incremento riguarda la copia locale e avviene dopo il return del valore); in main i=30, --i decrementa a 29 e stampa 29.
Macro
#define fa una sostituzione di testo prima della compilazione, senza valutare nulla.
Esempio 4 (esempio di appello 2020-21):
#define square(x) (x*x)
int i = 64/square(4); // diventa 64/(4*4)Il risultato è . Se la macro fosse #define square(x) x*x si avrebbe 64/4*4 = (64/4)*4 = 64; e square(a+1) con (x*x) diventa (a+1*a+1), non : le parentesi vanno messe attorno a ogni uso del parametro e al corpo della macro, ((x)*(x)).
Operatori su bit e interi
<<e>>: shiftscorrimento dei bit: a sinistra moltiplica per una potenza di 2, a destra divide a sinistra (moltiplica per ) e a destra (divide per ;>>su un intero con segno è aritmetico nei compilatori ARM comuni, suunsignedè logico). Per i dati a virgola fissa:(a*b)>>16.&,|,^,~: AND, OR, XOR, NOT bit a bit. Mascherare un campo:x & 0x0F; impostare il bit :x |= (1<<n); azzerare:x &= ~(1<<n); commutare:x ^= (1<<n); testare:(x>>n)&1.- Divisione e resto interi troncano verso zero:
-13/4 == -3,-13%4 == -1(come nella convenzione : Moltiplicatori veloci e divisionePer il controllo real-time serve un moltiplicatore a ciclo singolo: a look-up table (la tabella cresce come $2^{2n}\cdot2n$ bit, quindi si fa solo a pochi bit e si compongono prodotti da 4 bit: $A\cdot B=A_HB_H,2^{8}+(A_HB_L+A_LB_H)2^4+A_LB_L$) oppure a matrice (schiera di AND e sommatori, ritardo $\sim2n$; varianti a somma per colonne e di Wallace). La divisione hardware si fa con sottrazioni successive (con ripristino) su valori positivi e il segno alla fine ($D=Q,d+R$); con $n$ bit ci sono vincoli su dividendo e divisore. Senza divisore hardware: $Q=N/D$ con la serie $\frac{N(1+Z)(1+Z^2)\cdots}{1-Z^{2^n}}$, $D=1-Z$, $0{,}5<D<1$.Moltiplicatori veloci e divisione →). - Promozione di tipo. Gli operandi più corti di
intsono promossi aint(32 bit con segno) prima delle operazioni:uint16_t a=0xFFFF, b=0xFFFF; a*bè un prodotto traintche dà : overflow su intero con segno (comportamento indefinito). Si scrive(uint32_t)a*b. Per ottenere un prodotto a 32 bit di dueint16_tbasta(int32_t)a*b. Un castconversione esplicita di tipo, per esempio (uint32_t)a va fatto prima dell'operazione, non sul risultato. - Wrap-around senza segno:
uint16_t x = 65535; x++;vale 0 (come l'overflow del contatore di un timer: la sottrazione tra due letture dà il risultato giusto, Timer - prescaler, compare, capture e watchdogUn timer è un contatore binario di periodi di clock: $f_{cont}=f_{clk}/PSC$ ($PSC$ = fattore del prescaler). In modalità compare/match genera un evento (interruzione, reset del conteggio, azione su un pin) quando il conteggio raggiunge il registro di match: per un evento ogni $T$ servono $N=T,f_{clk}/PSC\le2^{16}$ conteggi, e si prende il minimo $PSC$ (massima risoluzione). Con uscita in modo toggle la frequenza dell'onda quadra è metà di quella degli eventi, il duty-cycle 50%. In modalità capture il valore del contatore, salvato a due fronti, dà il periodo: $T=C_2-C_1$ (corretto anche con un overflow), risoluzione $1/f_{clk}$. Il watchdog reinizializza il sistema se non viene azzerato in tempo.Timer - prescaler, compare, capture e watchdog →).
Programmi per µC
- Struttura tipica: inizializzazione (clock, periferiche), poi un
while(1){}; il lavoro nelle ISR. Una variabile condivisa tra ISRroutine di servizio di una interruzione e main va dichiarata{{volatile|parola chiave che dice al compilatore che la variabile può cambiare fuori dal flusso del programma, per esempio in una ISR}}(altrimenti il compilatore può tenerla in un registro e non vedere i cambiamenti). - I registri delle periferiche sono locazioni di memoria:
*(volatile uint32_t *)0x40021000 = 0x1;oppure tramite le strutture della libreria (TIMx->CNT,HRTIM1->sTimerxRegs[...].CPT1xR). - Buffer circolare (indirizzamento circolare in software): per una media mobile su 16 campioni si usa un vettore e un indice che riparte da 0:
yma -= xma[k]; yma += x; xma[k++] = x; if(k==16) k=0;. Un buffer di lunghezza potenza di 2 permettek = (k+1) & 15. - Un
printfnon esiste in un µC senza sistema operativo (si usa il debugger o la UART); gli esercizi d'esame usanoprintfsolo per chiedere un valore.
Errori comuni
- Scrivere
sizeof(pa)=32(bit) invece di 4 (byte). - Prendere il valore di
i++come quello dopo l'incremento. - Dimenticare che le macro non hanno tipi né parentesi implicite.
- Moltiplicare due
uint16_te pretendere unuint32_tcorretto senza il cast. - Dividere numeri interi negativi aspettandosi l'arrotondamento per difetto.
Versione ripasso
- Tipi (32 bit): char 1, short 2, int/long 4, long long 8, float 4, puntatore 4.
- Puntatori:
*pavalore,&aindirizzo;pa+1avanza disizeof(*pa).short a=10,*pa=&a:*pa=10,sizeof(pa)=4,sizeof(a)=2,(*pa)++stampa 10 (poi a=11),*(pa++)stampa 11. - Postfisso/prefisso:
i++valore prima;++idopo;doSomething(30)conreturn(i++)→ 30, poi--i= 29. - Macro: sostituzione di testo:
#define square(x) (x*x),64/square(4)=64/(4*4)= 4; senza parentesi 64. - Bit:
|=(1<<n),&=~(1<<n),^=;/e%verso zero (−13/4=−3, −13%4=−1); promozione aintcon segno: cast auint32_tprima del prodotto. - µC:
while(1){}+ ISR,volatile, registri come memoria, buffer circolare. - Errori: 32 bit invece di 4 byte;
i++dopo; macro senza parentesi; prodotto senza cast (Microcontrollori STM32 - clock, timer, ADC e codice HALI µC STM32 (core ARM Cortex-M) del corso si configurano con la libreria HAL: si riempiono strutture C (...InitTypeDef) con valori simbolici e si chiamano funzioni di libreria. Clock (STM32F334R8): oscillatore esterno HSE 8 MHz × PLL (MUL9) = SYSCLK 72 MHz (massimo); AHB ÷1, APB1 ÷2 (36 MHz), APB2 ÷1 (72 MHz); clock dell'ADC = clock del bus ÷ divisore (÷6 → 12 MHz). Timer in PWM: $f=f{clk}/((PSC+1)(Period+1))$,Pulse= duty $\times$ conteggi. Toggle: onda quadra a metà della frequenza di match. ADC: SAR a 12 bit, conversioni injected avviate da un trigger del timer, lettura con interruzione.Microcontrollori STM32 - clock, timer, ADC e codice HAL →).