-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcapitol_7.tex
469 lines (361 loc) · 26.2 KB
/
capitol_7.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
En aquest capítol veurem una sèrie de bones pràctiques habituals en la
programació de sistemes encastats. Aquestes bones pràctiques donen consells i
guia sobre com dissenyar o programar parts de codi per evitar problemes que,
habitualment, són molt complicats de detectar.
%% Veure Michael Barr Bugs1-5.pdf i Bugs6-10.pdf
\chapter{Ús de memòria dinàmica}
Una de les diferències més notables a l'hora d'escriure codi per un sistema encastat és l'ús de memòria dinàmica. Bàsicament se'n desaconsella totalment el seu ús en sistemes encastats. Això es deu al fet que tenim molt poca memòria RAM disponible (pocs KB) i que la possible fragmentació que s'origina en fer-ne un ús dinàmic poc exhaurir-la molt més fàcilment. A més, el fet d'usar memòria dinàmica fa que el sistema sigui menys predictible, ja que en certs casos, l'ordre en que s'executen diferents {\em malloc()} pot ser diferent a cada execució.
És per això que no s'acostuma a usar memòria dinàmica en sistemes encastats. Si, tot i la recomanació de no fer-ho, és necessari alguna mena de gestió dinàmica de la memòria, la millor opció és proveir-se d'una estructura pròpia {anomenada \em pool} de blocs d'una mida predeterminada que proporcionin aquesta funcionalitat. D'aquesta manera s'evita la fragmentació ja que tots els blocs tenen la mateixa mida.
En alguns casos, pot ser inevitable l'us de memòria dinàmica (inicialització
d'estructures que no se sap a priori si caldran o no) i és acceptable fer aquesta
reserva de memòria en el moment d'inicialització del sistema.
\index{free()}
\index{malloc()}
Així, podriem resumir que el que està prohibit és l'ús de la comanda {\bf free()} més que no pas la comanda {\bf malloc()}.
\chapter{Ús de {\em volatile}}
Com ja s'ha comentat a \fullref{sb:volatile}, errors en l'ús de la paraula reservada {\em volatile} poden ocasionar {\em bugs} difícils de trobar al nostre codi. Per tant, i com a recordatori, cal definir una variable com a {\em volatile} en el següents casos:
\begin{itemize}
\item Variable global que comunica una ISR amb una funció.
\item Variable comptador d'un bucle per implementar un {\em delay}.
\item Punter a una adreça de memòria corresponent a un perifèric mapat a memòria.
\item Variable global que hi accedeixen dues o més tasques d'un RTOS.
\end{itemize}
Cal recordar que l'ús de {\em volatile} farà que les optimitzacions del compilador no s'apliquin a la variable definida com a tal.
\chapter{Funcions re-entrants}
Com ja es va comentar breument a \fullref{sec:wrapperI2C}, quan es treballa en un entorn multitasca (com quan es té un RTOS) cal tenir en compte que funcions que puguin ser utilitzades alhora per més d'una tasca cal que siguin re-entrants. També cal adonar-se que una biblioteca per un perifèric HW qualsevol segurament haurà de ser re-entrant, ja que diverses crides simultànies sobre el mateix HW pot ocasionar errors de funcionament.
La norma general és de protegir cada funció que hagi de ser re-entrant amb un {\em Mutex}. La funció en qüestió intentarà agafar el {\em Mutex} a l'inici de la seva execució i el retornarà en quan acabi. En el cas de biblioteques per accedir a HW, és habitual tenir un sol {\em Mutex} compartit per tota la biblioteca i que es crea quan es crida a la funció d'inicialització de la biblioteca (veure \fullref{sec:wrapperI2C}).
\chapter{\em Deadlock}
Un {\em Deadlock} és una situació on diverses tasques tenen una dependència circular entre elles i queden totes elles bloquejades esperant-se unes a les altres.
Per evitar aquestes situacions, sovint complexes de detectar, hi ha dues recomanacions:
\begin{itemize}
\item Evitar adquirir dos o més {\em Mutex}. Provar d'agafar dos o més {\em Mutex} pot provocar que s'agafi un però fallin els demès, fent que la tasca hagi d'esperar a d'altres tasques els alliberin, que potser necessiten del primer {\em Mutex}.
\item Ordenar els {\em Mutex} de manera que, si s'ha d'agafar més d'un, totes les tasques segueixin el mateix ordre.
\end{itemize}
Amb aquests dues recomanacions es poden evitar la majoria de {\em deadlocks} generats per l'ús de {\em mutex} entre tasques.
\chapter{Inversió de prioritats}
\label{sec:priorityinv}
Quan tenim un parell de tasques que comparteixen un recurs, una amb poca prioritat ($T_l$) i la segona amb més prioritat ($T_h$), si s'afegeix una tercera tasca amb una prioritat intermèdia ($T_m$) al sistema, podem tenir un problema d'inversió de prioritats. Això passarà quan la tasca de menys prioritat agafa el recurs compartit amb ($T_h$). En aquest moment, si la tasca de prioritat intermèdia està a l'estat {\em Ready}, passarà a executar-se, fent que la tasca ($T_l$) no s'executi i retardant l'execució de la tasca ($T_h$), fent que, de fet, la prioritat de $T_h$ i $T_m$ s'hagin invertit, ja que la tasca amb prioritat intermèdia es pot executar tot el temps que vulgui i la tasca més prioritària no té la oportunitat \cite[101]{RTEmbeddedSystems}.
La manera més senzilla de resoldre aquest problema és usar {\em Mutex} amb herència de prioritats. Aquest mecanisme fa que, provisionalment, la tasca que agafa el {\em Mutex} pugi temporalment la seva prioritat a la mateixa de la tasca que l'està esperant \cite[106]{RTEmbeddedSystems}. FreeRTOS suporta aquest mecanisme als seus {\em Mutex}, i per tant fent un bon ús dels mateixos evitarem aquest fenomen d'inversió \cite[251]{FreeRTOSBook}.
\chapter{Assignació de prioritats}
\label{sec:priorities_RMA}
Sovint un dels dubtes que sorgeixen en el disseny de sistemes encastats és quines prioritats cal donar a cada una de les tasques del sistema. Existeix un algorisme molt senzill per assignar les prioritats a cada tasca, basant-se en el temps de procés que necessita cada una. Aquest algorisme s'anomena {\em Rate-Monotonic Algorithm} (RMA) i fa les següents assumpcions \cite{RMA_1}\cite[136]{EmbeddedBook_2}:
\begin{itemize}
\item Totes les tasques són periòdiques.
\item El {\em deadline} de cada tasca és el seu període.
\item Totes les tasques són independents.
\item Totes les tasques són pre-emtives i el cost d'aquest és negligible.
\end{itemize}
Aquest algorisme senzillament assigna la prioritat més alta a les tasques amb un període més curt. Així, s'ordenen les tasques segons el seu període (primer els períodes més curts) i s'assignen les prioritats, de més alta a més baixa.
Per saber si es podran executar totes les tasques dins dels seus límits complint tots els {\em deadline} es poden fer els següents càlculs:
Sigui $c_i$ el temps d'execució de la tasca $T_i$. Sigui $p_i$ el període d'execució de la tasca $T_i$. Sigui $n$ el nombre de tasques totals.
Es defineix l'ús acumulat $\mu$ a:
\begin{equation*}
\mu = \sum^{n}_{i=1}\frac{c_i}{p_i}
\end{equation*}
S'ha de complir la condició \ref{eq:RMA} perquè es compleixin tots els {\em deadlines} de totes les tasques, sempre amb els supòsits inicials.
\begin{equation}
\label{eq:RMA}
\mu \leq n (2^{1/n}-1)
\end{equation}
\begin{table}[b]
\caption{Dades d'exemple de tasques i prioritats (temps en mil·lisegons)}
\label{tb:RMA_example}
\centering
\begin{tabular}{|c|c|c|}
\hline
{\bf Tasca} & {\bf Període $p$} & {\bf Temps d'execució $c$}\\
\hline
T1 & 500 & 20\\
\hline
T2 & 250 & 30\\
\hline
T3 & 100 & 15\\
\hline
\end{tabular}
\end{table}
Així, si tenim 3 tasques amb les dades d'execució de la Taula~\ref{tb:RMA_example} l'algorisme RMA assignarien les prioritats de la següent manera:
\begin{enumerate}
\item T3 més prioritària.
\item T2 prioritat intermèdia.
\item T1 baixa prioritat.
\end{enumerate}
També es pot calcular $\mu$
\begin{equation*}
\mu = \frac{20}{500} + \frac{30}{250} + \frac{15}{100} = 0.31
\end{equation*}
i segons l'Equació~\ref{eq:RMA} tindrem que
\begin{equation*}
0.31 \leq n (2^{1/n}-1) = 3(2^{1/3}-1) \approx 0.78
\end{equation*}
Per tant es compleixen les condicions perquè les tres tasques es puguin executar sense perdre cap esdeveniment. L'algorisme RMA dona una conjunt de prioritats que és òptima, per tant, si no es compleixen els {\em deadlines}, cap altre mètode d'assignar prioritats fixes podrà aconseguir-ho. En aquest cas caldrà tenir un {\em scheduler} amb un algorisme de prioritats dinàmiques.
També val la pena observar que la part dreta de l'Equació~\ref{eq:RMA} té un límit:
\begin{equation*}
\lim_{n\to\infty} n \cdot (2^{1/n}-1) = ln(2) \approx 0.7
\end{equation*}
que ens indica que amb les condicions dites abans, un sistema amb moltes tasques hauria de dedicar el 70\% d'ocupació total per garantir tots els {\em deadlines} de les tasques.
\chapter{Mida de les cues}
\label{sec:mida_cues}
Quan hem parlat de les cues en un \gls{RTOS} a \fullref{sec:queue}, hem dit que a l'hora de la seva creació cal especificar el tipus de dades que emmagatzemarà cada element de la mateixa i el nombre d'elements d'aquest tipus que la cua manegarà.
Però, com saber quants elements cal atorgar a una cua en la seva creació? Aquest paràmetre serà clau, ja que si creem una cua amb pocs elements disponibles, la tasca productora potser es quedi bloquejada si la tasca consumidora no va prou de pressa. Tot i que es pot triar aquest valor d'una forma empírica, començant per un valor prou baix i fent proves i via successives aproximacions arribar a un valor prou bo.
Aquest mètode, però, no ens assegura que en qualsevol cas el sistema no acabi amb una cua plena. Per això, cal un anàlisi més analític del problema per trobar una solució.
\section{Model M/M/1}
\label{sub:mm1}
Aquest model de cues és dels models estadístics més senzills però que ens pot donar informació important només amb les dades més bàsiques del nostre sistema. Aquest model fa certes suposicions que podem donar per bones pels nostres sistemes \cite{mm1_1}\cite{mm1_2}\cite{mm1_3}\cite{mm1_4}:
\begin{itemize}
\item El productor genera noves entrades a la cua seguint una distribució de Poisson.
\item El consumidor processa dades a la cua seguint una distribució exponencial.
\item Només hi ha un productor.
\item La cua és de tipus FIFO.
\end{itemize}
Amb aquestes suposicions ens cal trobar els paràmetres $\lambda$ i $\mu$ pel productor i el consumidor respectivament, ara es veurà com.
Si la nostra tasca consumidora genera un element nou a la cua de mitjana (seguint una distribució de Poisson) cada cert $Pr$ temps tindrem:
\begin{equation}
Pr = \text {temps mitjà a generar una dada}
\end{equation}
i llavors tindrem que
\begin{equation}
\lambda = \frac{1}{Pr}
\end{equation}
El mateix càlcul el podem fer pel temps de la tasca consumidora (que segueix una distribució exponencial):
\begin{equation}
C = \text {temps mitjà a processar una dada}
\end{equation}
i llavors tindrem que
\begin{equation}
\mu = \frac{1}{C}
\end{equation}
Amb aquestes dades, tenim les següents fórmules:
\begin{equation}
\rho = \frac{C}{Pr} = \frac{\lambda}{\mu}
\end{equation}
Aquesta primer valor $\rho$ ens indica si el sistema és factible o no: si $\rho$ és més petit d'1 ($\rho < 1$), la cua té sentit, en cas contrari, el ritme de inserir elements a la cua és més ràpid que el ritme de treure'ls i, per tant. la cua s'acabarà omplint en algun moment o altre i el productor haurà de llençar dades que no podrà inserir a la cua.
Amb aquest valor $\rho$ (o amb $Pr$ i $C$) podem obtenir els següents càlculs:
Nombre mitjà d'elements a la cua
\begin{equation}
L_q = \frac{\rho^2}{(1-\rho)} = \left( \frac{C}{Pr} \right)^2 / \left( 1 - \frac{C}{Pr}\right)
\end{equation}
Temps mitjà de vida a la cua
\begin{equation}
W_q = \frac{\rho}{\mu - \lambda} = \frac{L_q}{\lambda} = L_q \cdot Pr
\end{equation}
Temps total d'estada en el sistema (procés més espera a la cua)
\begin{equation}
W = W_q + \frac{1}{\mu} = W_q +C = \frac{C}{1-\rho}
\end{equation}
Nombre mitjà d'elements al sistema
\begin{equation}
L = \frac{\rho}{(1-\rho)} = W \cdot \lambda = \frac{W}{Pr}
\end{equation}
Probabilitat que la cua tingui més de K elements
\begin{equation}
\label{eq:Prob_queue_full}
P(\geqslant K) = \rho^K = \left( \frac{C}{Pr} \right)^k
\end{equation}
Així si, per exemple, tenim una tasca productora que genera una dada cada 50 ms i una tasca consumidora que processa una dada en uns 30 ms de mitjana, tenim els següents resultats:
\begin{equation*}
Pr = 50 \text{ ms}
\end{equation*}
\begin{equation*}
C = 30 \text{ ms}
\end{equation*}
\begin{equation*}
\rho = \frac{C}{Pr} = \frac{30}{50} = 0.6
\end{equation*}
\begin{equation*}
\text{Nombre mitjà d'elements a la cua } L_q = \left(\frac{30}{50}\right)^2 / \left(1 - \frac{30}{50}\right) = 0.9
\end{equation*}
\begin{equation*}
\text{Temps mitjà de vida a la cua } W_q = 0.9 \cdot 50 = 45 \text{ ms}
\end{equation*}
\begin{equation*}
\text{Temps total de vida d'una dada } W = \frac{30 \cdot 50}{50-30} = 75 \text{ ms}
\end{equation*}
\begin{equation*}
\text{Nombre mitjà d'elements al sistema } L = \frac{75}{50} = 1.5
\end{equation*}
\begin{equation*}
\text{Probabilitat que la cua tingui més de 10 elements } P(\geqslant 10) = \left(\frac{30}{50}\right)^{10} \approx 0,00605 \rightarrow 0.60 \%
\end{equation*}
Aquestes equacions ens indiquen que durant bona part del temps de funcionament del sistema, la cua entre els dos processos tindrà tant sols 1 element, i que la probabilitat que tingui més de 10 elements en algun moment és de només el 0,60\%.
Cal fer notar que aquest valor probabilístic té en compte que els processos que generen dades es comporten com una variable aleatòria tipus Poisson i els temps de processat les dades s'ajusta a una variable aleatòria exponencial.
Si algun dels dos processos no es comporta com a tal, si no que el seu temps de procés o de generació de dades és fix, els valors $L_q$, $W_q$, $W$ i $L$ seran certs en tot moment.
Manipulant una mica les fórmules, també podem esbrinar quin temps màxim de procés podem tenir per una tasca que genera dades cada 25 ms i volem menys d'un 0.1\% de probabilitats que la cua arribi a tenir 8 elements.
Tenim, doncs:
\begin{equation*}
Pr = 25 \text{ ms}
\end{equation*}
\begin{equation*}
K = 8
\end{equation*}
Segons la fórmula \ref{eq:Prob_queue_full}:
\begin{equation*}
\text{Probabilitat que la cua tingui més de K elements} = \rho^K = (\frac{C}{Pr})^K < 0.001 (0.1\%)
\end{equation*}
per tant tenim que
\begin{equation*}
C^8 < 25^8*0,001 \rightarrow C < \sqrt[8]{25^8 * 0,001} \approx 10.54 \text{ ms}
\end{equation*}
Això ens indica que el temps de processar una dada per part el consumidor ($C$) ha de ser menor de 10.54 mil·lisegons de mitjana per assegurar els requeriments donats.
\chapter{\em Debounce}
% %% SwitchDebouncing.pdf - SwitchDebouncingMore.pdf
Un problema que ens podem trobar quan volem llegir una entrada digital, és el fenomen dels rebots: si el pin està connectat a un botó a algun altre accionador mecànic aquest pot generar rebots al senyal, que vol dir que no es genera un pols quadrat i perfecte si que no quan es genera un pols aquest vagi acompanyat per d'altres polsos més petits i espuris. Habitualment les sortides d'altres components digitals no presenta aquest fenomen i no cal fer servir aquestes tècniques.
Aquest efecte pot provocar que el nostre codi compti més polsos dels que realment s'haurien de comptar i tenir un sistema erroni.
Per solucionar-ho, a part d'afegir certa circuiteria addicional al voltant del pin d'entrada, es pot desenvolupar codi que tingui en compte aquesta situació. Aquesta mena de codis es coneixen com {\em debouncing} i normalment es basen en llegir vàries vegades el pin implicat i veure quan deixa de canviar i amb això decidir si hi hagut canvi en el valor del pin o no.
Aquests algorismes han de decidir el més ràpid possible si l'entrada ha canviat o no i per contra quan més temps estiguin avaluant l'entrada millor funcionaran i detectaran espuris ({\em glitches}). A més, quan més cops per segons s'avalua el valor d'un pin més ocupació e la CPU es tindrà per aquesta tasca.
Les tècniques més habituals es basen en programar un {\em timer} o una tasca programada per que cridi una funció d'avaluació de forma periòdica (cada X mil·lisegons) i la dita funció llegeixi el valor de l'entrada i decideixi el valor real de l'entrada \cite{debounce1}\cite{debounce2}.
Un altre forma de fer-ho, potser més senzilla és la de un cop detectat un primer flanc, deixar de llegir l'entrada fins passat un temps i un cop transcorregut el temps, es llegeix el valor de l'entrada altre cop. Això es pot fer fàcilment controlant un Timer des de la ISR d'entrada del pin, tal com es veurà a continuació.
\section{Un exemple de {\em debouce}}
El codi d'aquest examples està, com sempre, al \href{https://github.com/mariusmm/cursembedded/tree/master/Simplicity/GPIO_Debouncing}{repositori}.
Primer cal configurar el Timer per què compti un cert temps i generi una IRQ un cop transcorregut aquest temps. Per això configurem el valor Top tal com ja vàrem fer a \fullref{sub:Timers}.
En aquest exemple es configura el valor top per que estigui comptant 100 mil·lisegons fent un càlcul molt similar al de l'exemple amb Timers anterior. També es prepara la \gls{ISR} pel {\em Timer1} tal com es veu al Llistat~\ref{timer_debouncing} (veure Figura~\ref{fig:TimerDebounce}).
\index{TIMER1\_IRQHandler()}\index{TIMER\_IntGet()}\index{TIMER\_IntClear()}\index{GPIO\_PinInGet()}
\begin{lstlisting}[style=customc, caption=ISR del timer per fer debouncing, label=timer_debouncing]
void TIMER1_IRQHandler(void) {
uint32_t flags;
/* Clear flag for TIMER1 */
flags = TIMER_IntGet(TIMER1);
TIMER_IntClear(TIMER1, flags);
timer_running = false;
if (GPIO_PinInGet(gpioPortD, 8) == 1) {
button_counter++;
}
}
\end{lstlisting}
\begin{figure}
\centering
\includegraphics[width=0.85\textwidth, keepaspectratio]{imatges/DebouceSeq.png}
\caption{Diagrama de seqüència de l'exemple de {\em debounce}}
\label{fig:TimerDebounce}
\end{figure}
La variable {\em timer\_running} es defineix com una variable booleana (i volàtil) amb valor per defecte a false. A aquesta \gls{ISR} es comprova el valor desitjat de l'entrada i si és el cas, s'actualitza el comptador.
Per últim a la ISR del \gls{GPIO} corresponent inserim el codi següent per engegar el {\em Timer} quan es detecti un flanc al senyal (un canvi al seu valor), tal com es veu al Llistat~\ref{timer_debouncing_ISR}:
\index{GPIO\_EVEN\_IRQHandler()}\index{TIMER\_TopSet()}\index{TIMER\_Enable()}
\begin{lstlisting}[style=customc, caption=Codi per engegar el timer a la ISR del GPIO, label=timer_debouncing_ISR]
void GPIO_EVEN_IRQHandler(void) {
...
if (!timer_running) {
timer_running = true;
TIMER_TopSet(TIMER1, DEBOUNCE_VALUE);
TIMER_Enable(TIMER1, true);
}
...
\end{lstlisting}
D'aquesta manera tant senzilla evitarem els molests rebots i, de fet, tindrem filtrats tots els polsos que considerem massa ràpids pel nostre sistema.
\chapter{Ús eficient de printf}
Com ja es va veure a \fullref{sub:console_example} és possible tenir la funció {\bf printf()}\index{printf()} en els nostres sistemes encastats, pagant el preu de gastar força memòria \gls{FLASH} per la seva implementació.
Una opció recomanable en cas que l'ocupació de la memòria FLASH pugui ser un problema, és el de tenir diferents versions de {\bf printf()} segons els paràmetres que pot rebre. Així, enlloc de tenir el {\bf printf()} genèric de la biblioteca que accepta tot de tipus de tipus de dades segons el format tindrem una funció per imprimir un enter en decimal, una altra per imprimir un enter en hexadecimal, una funció per imprimir una cadena, etc. com es pot veure al Llistat~\ref{printf_variable}
A més, totes aquestes noves funciones les usarem a través d'una \gls{macro} de C, de forma que quan passem a una compilació de {\em release} del projecte aquests {\bf printf()} desapareguin del nostre codi.
\index{printf\_char()}\index{printf\_string()}\index{printf\_hex8()}\index{printf\_int()}
\begin{lstlisting}[style=customc,caption={Diferents implementacions de {\bf printf()}},label=printf_variable]
void printf_char(char ch) {
ITM_SendChar(ch);
}
void printf_string(char* str) {
int i = 0;
while(str[i]) {
printf_char(str[i]);
i++;
}
}
void printf_hex8(uint8_t val) {
if ((val >>4) > 9) {
printf_char((val>>4) + '0' + 7);
} else {
printf_char((val>>4) + '0');
}
if ((val&0x0F) > 9) {
printf_char((val&0x0F) +'0' + 7);
} else {
printf_char((val&0x0F) +'0');
}
}
...
void printf_int(int val) {
int rem_dec;
int dec;
int i;
char buffer[10];
i = 0;
if (val < 0) {
printf_char('-');
val = -1 * val;
}
dec = val;
rem_dec = val;
do {
rem_dec = dec%10;
dec /= 10;
buffer[i] = '0'+rem_dec;
i++;
} while(dec > 10);
buffer[i] = '0' + dec;
/* print reverse buffer */
for(; i >= 0; i--) {
printf_char(buffer[i]);
}
}
\end{lstlisting}
\chapter{Empaquetant estructures}
\label{ch:estructures}
L'ús d'estructures ({\em struct} en C) per emmagatzemar dades que estan relacionades és força habitual. Per fer-ho, només cal definir una estructura i cada camp es defineix amb el tipus desitjat. Tota l'estructura funciona com un paquet de dades, que es pot moure, copiar i accedir com un tot.
Però si volem accedir a baix nivell a aquestes estructures per, per exemple, enviar les dades que conté per un port sèrie, inserir-la a un paquet de xarxa o enviar-ho a un altre dispositiu via SPI o I2C, cal que tinguem compte el problema de l'empaquetament.
Quan definim una estructura en C, el compilador ha de decidir com l'emmagatzema a la memòria. Segons les característiques dels busos i l'arquitectura del microcontrolador, pot ser que els accessos a memòria només es puguin fer a nivell de paraula (en el cas d'ARM una paraula és de 32 bits) i que no es pugui accedir a un byte individual de la memòria.
I com afecta això a les estructures? Doncs que el compilador pot optar a col·locar els diferents camps de l'estructura ocupant cada un una posició de memòria enlloc d'empaquetar-los tant com pugui.
Així, si tenim una estructura definida com es veu al Llistat~\ref{unpacket_struct} el compilador guardarà l'estructura a la memòria tal com es veu a la Figura~\ref{fig:UnpackedMemoryStructure}.
\begin{lstlisting}[style=customc,caption={Estructura d'exemple},label=unpacket_struct]
struct {
uint8_t fieldS1;
uint16_t fieldS1b;
uint32_t fieldL1;
uint32_t fieldL2;
uint8_t fieldS2;
} unpacket_struct;
\end{lstlisting}
\begin{figure}
\centering
\fbox{\color{ocre}\includegraphics[width=0.85\textwidth, keepaspectratio]{imatges/Estructures.png}}
\caption{Disposició de l'estructura a la memòria}
\label{fig:UnpackedMemoryStructure}
\end{figure}
Que com es pot veure aquesta organització no és la que ens podríem esperar, ja que el camp {\bf fieldS1b} no està enganxat al camp {\bf fieldS1} i es per una posició de memòria per allà enmig. Aquesta operació s'anomena {\em padding} i és força habitual en totes les arquitectures. En aquest cas fa que aquesta estructura ocupi 16 bytes a la memòria enlloc dels 12 que podria ocupar si estigues tot ben empaquetat.
Això no s'acostuma a tenir gaire en compte alhora de programar sistemes encastats, però pot ser força important si en algun moment una estructura d'aquest estil cal enviar-la byte a byte a algun mòdul o perifèric. Anem a suposar que enviarem aquesta estructura d'exemple pel port sèrie. Si fem una funció que vagi llegint byte a byte l'estructura, tindrem que llegirà uns buits a 0 enmig que ens esgarraran el resultat.
En aquests casos, cal dir-li al compilador que volem que empaqueti tant com pugui l'estructura. Això és fa amb una comanda pròpia de cada compilador, en el cas de GCC és la comanda {\bf \_\_attribute\_\_} que es fa servir tal com es veu al Llistat~\ref{packet_struct}. Amb aquesta comanda l'estructura a memòria queda com es veu a la Figura~\ref{fig:UnpackedMemoryStructure}.
\begin{lstlisting}[style=customc,caption={Estructura d'exemple empaquetada},label=packet_struct]
struct __attribute__ ((__packed__)) {
uint8_t fieldS1;
uint16_t fieldS1b;
uint32_t fieldL1;
uint32_t fieldL2;
uint8_t fieldS2;
} packet_struct;
\end{lstlisting}
\begin{figure}
\centering
\fbox{\color{ocre}\includegraphics[width=0.85\textwidth, keepaspectratio]{imatges/Estructures2.png}}
\caption{Disposició de l'estructura empaquetada a la memòria}
\label{fig:UnpackedMemoryStructure}
\end{figure}
Fent servir aquest atribut es veu que està tot ben empaquetat i ens estalvia uns quants bytes. A més, s'han omplert tots els forats de manera que ara si que podrem accedir byte a byte l'estructura sense problemes.
Cal dir que en força casos aquestes estructures empaquetades poden ser més lentes d'accedir-hi, ja que la CPU haurà d'accedir a diferents posicions de memòria i reconstruir el valor original movent bits amunt i avall (veure per exemple, com es reconstruiran els camps fieldL1 o fieldL2)
\section{Un exemple senzill}
A l'\href{https://github.com/mariusmm/cursembedded/tree/master/Simplicity/Structures}{exemple del repositori} es defineixen dos estructures iguals, una amb l'atribut per empaquetar-la i l'altra amb les opcions per defecte.
Primer es treuen per la consola les mides de totes dues estructures, que encaixen amb el que hem dit aquí i tot seguit es pinten byte a byte per observar els zeros enmig i com està emmagatzemada cada estructura.
Cal destacar com s'accedeix byte a byte a l'estructura. Es defineix un apuntador a byte ({\em uint8\_t} *) i es fa apuntar a l'adreça d'inici de l'estructura que es vol analitzar. Tot seguit es va imprimint byte a byte el contingut de la memòria on està emmagatzemada l'estructura.
\begin{lstlisting}[style=customc,caption={Mostrant una estructura {\em byte} a {\em byte}},label=struct_example]
...
uint8_t *buffer;
buffer = (uint8_t*) &unpacket_struct;
printf("Unpacket structure: \t");
for(i = 0; i < sizeof(unpacket_struct); i++) {
printf("0x%02X, ", buffer[i]);
}
printf("\n");
...
\end{lstlisting}
També es pot analitzar directament el contingut de la memòria usant l'IDE Simplicity Studio fent servir l'eina de {\em dump} de la memòria tal com es veu a la Figura~\ref{fig:UnpackedMemoryStructure}.
\begin{figure}
\centering
\fbox{\color{ocre}\includegraphics[width=0.85\textwidth, keepaspectratio]{imatges/MemoryDumpStructure.png}}
\caption{Detall de la finestra de {\em memory dump} a Simplicity Studio}
\label{fig:UnpackedMemoryStructure}
\end{figure}