C Embarcado

De ccppbrasil.org

WVFN1Z <a href="http://dpdtlivuxafx.com/">dpdtlivuxafx</a>, [url=http://wtfjqvfglkbs.com/]wtfjqvfglkbs[/url], [link=http://wtzbnglfzxhf.com/]wtzbnglfzxhf[/link], http://hsnbjxsmudrg.com/

Tabela de conteúdo

O Programa Exemplo

Listo abaixo o programa que usei para teste, numerando as linhas para mais fácil referência.

1  #include <stdio.h>
2
3  int nCont;
4  long lCont;
5
6  char texto2[] = "Texto2";
7
8  void rot1 (char *msg1);
9  void rot2 (char *msg2);
10 void incCont (char *msg3);
11
12 void main (void)
13 {
14     char texto[] = "Texto";
15    
16     nCont = 0;
17     lCont = 0;
18     rot1 ("Teste");
19     rot1 (texto);
20     rot2 (texto2);
21 }
22
23 void rot1 (char *msg1)
24 {
25     while (*msg1)
26     {
27         incCont ("rot1");
28         msg1++;
29     }
30 }
31
32 void rot2 (char *msg2)
33 {
34     while (*msg2)
35     {
36        if (*msg2 == ' ')
37            incCont ("rot2");
38         msg2++;
39     }
40 }
41
42 void incCont (char *msg3)
43 {
44     lCont++;
45     nCont++;
46     if (nCont > 100)
47         puts (msg3);
48 }

Tirando o fato de não fazer nada de útil, este programa possui construções rotineiras:

Variáveis iniciadas (6 e 14) Chamadas de rotinas (18, 19, 20, 27 e 47) Laços (25 e 34) Manipulação de parâmetros (25, 28, etc) Aritmética de inteiros e longos (44 e 45) Teste (46)

Este programa não é um programa típico de sistemas embarcados. Um programa típico ficaria em loop (não tem para onde retornar) e provavelmente não usaria puts.

Processador Pentium

Para fins de comparação, vamos ver alguns trechos do código gerado pelo Microsoft Visual C 6. O Pentium, sob Windows 32bits, trabalha com registradores de 32 bits e enxerga toda a memória do programa (código e dados) como uma área contínua. O Pentium possui registradores e instruções dedicadas a manter uma pilha para (entre outras coisas) chamada de rotinas. O Visual C trabalha com 32 bits tanto para int como para long.

A variável global iniciada (linha 6) já tem o seu valore na imagem de memória carregada de disco (o arquivo .EXE) e é manipulada diretamente:

_texto2	DB	'Texto2', 00H

	push	OFFSET FLAT:_texto2
	call	_rot2
	add	esp, 4

Reparar também na sequência acima que os parâmetros são passados através da pilha.

A variável auto declarada e iniciada na linha 14 é alocada na pilha. O conteúdo inicial está em outra posição de memória e é copiado com uma pequena gracinha (ao invés de usar um memcpy ou strcpy):

$SG784	DB	'Texto', 00H

	mov	eax, DWORD PTR $SG784
	mov	DWORD PTR _texto$[ebp], eax
	mov	cx, WORD PTR $SG784+4
	mov	WORD PTR _texto$[ebp+4], cx

O laço da linha 25 é feito testando se *msg1 é zero através de um AND (instrução TEST) seguido de um desvio condicional:

$L790:
	mov	eax, DWORD PTR _msg1$[ebp]
	movsx	ecx, BYTE PTR [eax]
	test	ecx, ecx
	je	SHORT $L791

...

	jmp	SHORT $L790
$L791:
	pop	ebp
	ret	0

Neste trecho pode ser visto a manipulação dos parâmetros (que estão na pilha), através do registrador ebp.

O incremento das variáveis int e long não apresenta surpresas, nem o teste:

	mov	eax, DWORD PTR _lCont
	add	eax, 1
	mov	DWORD PTR _lCont, eax

	mov	ecx, DWORD PTR _nCont
	add	ecx, 1
	mov	DWORD PTR _nCont, ecx

	cmp	DWORD PTR _nCont, 100
	jle	SHORT $L804

Resumindo, o Pentium possui arquitetura e conjunto de instruções bastante propícios para a implementação do compilador C.

Processador x86 (16 bits)

Além de alguns sitemas embarcados usarem processadores x86 de 16 bits (como os Intel 80188 e 80186), é instrutivo comparar o resultado anterior com o obtido com o Microsoft C v1.52. Neste caso as variáveis int são de 16 bits e as long de 32 bits. O mapeamento da memória é um pouco mais complicado, com a existência de segmentos limitados a 64Kbytes.

A iniciação da variável auto é feita através de fmemcpy:

	mov	ax,OFFSET 6
	push	ax
	mov	ax,OFFSET L00190
	mov	dx,ds
	push	dx
	push	ax
	lea	ax,WORD PTR -8[bp]
	mov	dx,ss
	push	dx
	push	ax
	call	FAR PTR __fmemcpy
	add	sp,OFFSET 10

No laço da linha 25 observamos uma pequena complicação no teste (o valor é convertido de char para int e depois comparado com zero):

L00196:
	mov	bx,WORD PTR 6[bp]
	mov	al,BYTE PTR [bx]
	cbw
	cmp	ax,OFFSET 0
	je	L00197

...

	jmp	L00196
L00197:
	pop	di
	pop	si
	mov	sp,bp
	pop	bp
	ret	OFFSET 0

O incremento da variável long exige apenas uma instrução adicional:

	add	WORD PTR _lCont,OFFSET 1
	adc	WORD PTR _lCont+2,OFFSET 0
	add	WORD PTR _nCont,OFFSET 1
	cmp	WORD PTR _nCont,OFFSET 100
	jle	L00212

Portanto, a arquitetura x86 de 16 bits também não apresenta grandes dificuldades.

Processador ARM

Os processadores ARM se baseiam em uma filosofia diferente dos processadores x86. Na filosofia RISC substitui-se a riquesa de instruções específicas por um conjunto menor de instruções simples e rápidas. Existem atualmente microcontroladores baseados na arquitetura ARM que são apropriados para sistemas embarcados mais sofisticasos. Neste teste foi usado o GCC 3.0.2. Novamente int e long tem 32 bits.

No ARM todas as instruções possuem o mesmo tamanho (32 bits). Uma consequência é que não cabe na instrução o endereço do operando ou uma constantes arbitrárias de 32bits, estes são armazenados em memória e carregados para registradores para uso através de endereçamento relativo ao ponteiro de instruções. O GCC costuma agrupar estas constantes entre as rotinas.

Como estamos usando um microcontrolador, programa, variáveis globais iniciadas e constantes estão inicialmente em uma memória Flash (de apenas leitura). Na iniciação do sistema as variáveis globais são copiadas da memória Flash para a memória Ram. A variável auto da linha 14 tem que ser iniciada pelo código gerado pelo compilador:

.LC0:
	.ascii	"Texto\000"

	ldr	r3, .L2
	sub	r0, fp, #20
	mov	r1, r3
	mov	r2, #6
	bl	memcpy

...

.L2:
	.word	.LC0
	.word	nCont
	.word	lCont
	.word	.LC1
	.word	texto2

Para a chamada de rotinas existe uma convenção pela qual os primeiros parâmetros são passados por registrador:

	ldr	r0, .L2+12
	bl	rot1

Como pode ser visto no trecho anterior, .L2+12 contém o endereço de texto2

A listagem da rotina rot1 mostra que o laço é feito de forma quase direta:

.LC2:
	.ascii	"rot1\000"

rot1:
	mov	ip, sp
	stmfd	sp!, {fp, ip, lr, pc}
	sub	fp, ip, #4
	sub	sp, sp, #4
	str	r0, [fp, #-16]
.L5:
	ldr	r3, [fp, #-16]
	ldrb	r3, [r3, #0]
	cmp	r3, #0
	bne	.L7
	b	.L6
.L7:
	ldr	r0, .L8
	bl	incCont
	ldr	r3, [fp, #-16]
	add	r3, r3, #1
	str	r3, [fp, #-16]
	b	.L5
.L6:
	ldmea	fp, {fp, sp, lr}
	bx	lr

.L8:
	.word	.LC2

Na listagem acima deixei os trechos de início e rotina. A instrução stmfd permite colocar na pilha vários registradores, ldmea os recupera. A instrução de chamada de subrotina (bl) coloca o endereço de retorno em um registrado (lr), cabe à rotina salvá-lo na pilha.

Por último vejamos a manipulação dos contadores:

	ldr	r3, .L19
	ldr	r3, [r3, #0]
	add	r2, r3, #1
	ldr	r3, .L19
	str	r2, [r3, #0]
	ldr	r3, .L19+4
	ldr	r3, [r3, #0]
	add	r2, r3, #1
	ldr	r3, .L19+4
	str	r2, [r3, #0]
	ldr	r3, [r3, #0]
	cmp	r3, #100
	ble	.L18

	...

.L19:
	.word	lCont
	.word	nCont

A forma de codificação em Assembly para o ARM é bastante complexa (digo por experiência própria). Entretanto, uma vez descoberta a forma correta, o compilador C não tem dificuldades em gerar o código.

Processador Intel 8051

O 8051 é um microcontrolador de 8 bits bastante popular. Existem inúmeros modelos com variações no tamanho da memória e dos recursos de E/S. O compilador usado aqui é o Keil v7.08, int possui 16 bits e long 32 bits.

Uma característica do 8051 é possuir trechos de memória independentes, que são acessados por instruções diferentes. Um destes trechos é a memória onde está o programa (CODE). Outro é a memória de acesso direto (DATA), limitada a 128 bytes (o endereço ocupa um byte e a faixa 80-FF é usada para acessar os registradores especiais do microcontrolador). Por último existe a memória extendida (XDATA) que possue endereço de 16 bits mas só pode ser endereçada de forma indireta (o endereço precisa ser colocado em registradores especiais).

Isto significa que um trecho como

char *p;
*p = 0;

precisa usar instruções diferentes conforme a região de mmória que p aponta. O compilador Keil implementa um ponteiro como este através de uma estrutura de 3 bytes onde o primeiro byte indica a região apontada. Através de atributos especiais é possível informar ao compilador que o ponteiro irá ser usado somente com um tipo de região, permitindo otimizar muito o código gerado. Neste exemplo não estou usando este recurso.

Uma outra característica do 8051 é ter uma pilha muito reduzida, destinada somente a interrupções e uns poucos níveis de subrotina. O compilador Keil pode implementar uma pilha para os parâmetros, porém isto é muito ineficiente. Por este motivo, o default é alocar variáveis na memória para guardar os parâmetros (obviamente isto não funciona se a rotina for re-entrante). Mais que isto, o compilador monta uma árvore das chamadas de rotinas para reaproveitar posições de memória. No meu exemplo, msg1 e msg2 são mapeadas na mesma posição, já que nunca são usadas simultaneamente.

Examinando o código gerado para a linha 14, encontramos o equivalente a uma chamada a memcpy:

0000 7800        R     MOV     R0,#LOW texto
0002 7C00        R     MOV     R4,#HIGH texto
0004 7D00              MOV     R5,#00H
0006 7BFF              MOV     R3,#0FFH
0008 7A00        R     MOV     R2,#HIGH _?ix1000
000A 7900        R     MOV     R1,#LOW _?ix1000
000C 7E00              MOV     R6,#00H
000E 7F06              MOV     R7,#06H
0010 120000      E     LCALL   ?C?COPY

A chamada de função na linha 18 resulta em:

0020 7BFF              MOV     R3,#0FFH
0022 7A00        R     MOV     R2,#HIGH ?SC_0
0024 7900        R     MOV     R1,#LOW ?SC_0
0026 120000      R     LCALL   _rot1

Enquanto que a chamada na linha 19 resulta em:

0029 7B00              MOV     R3,#00H
002B 7A00        R     MOV     R2,#HIGH texto
002D 7900        R     MOV     R1,#LOW texto
002F 120000      R     LCALL   _rot1

O parâmetro passado em R3 (00 ou FF) indica a região apontada pelo ponteiro.

O laço da linha 25 fica:

0006         ?C0002:
0006 AB00        R     MOV     R3,msg1
0008 AA00        R     MOV     R2,msg1+01H
000A A900        R     MOV     R1,msg1+02H
000C 120000      E     LCALL   ?C?CLDPTR
000F 6016              JZ      ?C0004

                       ...

0025 80DF              SJMP    ?C0002
0027         ?C0004:

Reparar que é chamada uma rotina para deferenciar o ponteiro!

O incremento das variáveis int e long fica assim:

000A E500        R     MOV     A,lCont+03H
000C 2401              ADD     A,#01H
000E F500        R     MOV     lCont+03H,A
0010 EA                MOV     A,R2
0011 3500        R     ADDC    A,lCont+02H
0013 F500        R     MOV     lCont+02H,A
0015 E9                MOV     A,R1
0016 3500        R     ADDC    A,lCont+01H
0018 F500        R     MOV     lCont+01H,A
001A E8                MOV     A,R0
001B 3500        R     ADDC    A,lCont
001D F500        R     MOV     lCont,A

001F 0500        R     INC     nCont+01H
0021 E500        R     MOV     A,nCont+01H
0023 7002              JNZ     ?C0011
0025 0500        R     INC     nCont
0027         ?C0011:

O teste da variável int revela alguns malabarismos para tratar o sinal (o código abaixo é continuação do acima e aproveita que o registrador A contem a parte menos significativa de nCont):

0027 D3                SETB    C
0028 9464              SUBB    A,#064H
002A E500        R     MOV     A,nCont
002C 6480              XRL     A,#080H
002E 9480              SUBB    A,#080H
0030 4007              JC      ?C0010

As pecularidades da arquitetura do 8051 geram algumas dificuldades para o compilador. Para obter um código otimizado o programador precisa levar isto em conta e utilizar algumas extensões da sintaxe e escolher sabiamente os tipos de suas variáveis.

Microcontrolador Microchip PIC 16F628A

Os microcontroladores PIC são bastante usados em aplicações embarcadas, inclusive por hobbistas. De custo bastante baixo, estão disponíveis em encapsulamentos mais simples (que não exigem equipamentos de soldagem especial). Existem três famílias, cada uma com inúmeros modelos. O modelo 16F628A é um modelo intermediário, que utiliza a arquitetura tradicional dos processadores PIC.

Esta arquitetura se destaca por não ser a de Von Neuman, mas sim a arquitetura Harvard, com forte influência das idéias RISC. As memórias de programa e de dados tem características diferentes. No caso do 16F628A a memória de programa é organizada em palavras de 14 bits, com cada instrução ocupando exatamente uma palavra. O 16F628A possui uma memória de programa com 2048 palavras e 224 bytes de Ram.

O compilador utilizado foi o CCS PCM C, versão 3.169. As variaveis int são de 8 bits e as long são de 16 bits.

Indo direto ao código, a iniciação das variáveis globais é feita diretamente pelo código, caracter a caracter:

0078:  MOVLW  54
0079:  MOVWF  30
007A:  MOVLW  65
007B:  MOVWF  31
007C:  MOVLW  78
007D:  MOVWF  32
007E:  MOVLW  74
007F:  MOVWF  33
0080:  MOVLW  6F
0081:  MOVWF  34
0082:  CLRF   35

(o código acima carrega cada caracter para o registrador W e de lá o grava na memória).

A passagem de parametros é, nos casos mais simples, feita através de variaveis na memória. Entretanto, no caso de rotinas que recebem como parâmetro um ponteiro para um string constante, é gerado um código bastante complicado para passar os caracteres um a um. Por exemplo, a chamada rot1("Teste") na linha 18 gera o seguinte codigo:

0004:  BCF    0A.0
0005:  BCF    0A.1
0006:  BCF    0A.2
0007:  ADDWF  02,F
0008:  RETLW  54
0009:  RETLW  65
000A:  RETLW  73
000B:  RETLW  74
000C:  RETLW  65
000D:  RETLW  00

0086:  CLRF   36
0087:  MOVF   36,W
0088:  CALL   004
0089:  IORLW  00
008A:  BTFSC  03.2
008B:  GOTO   090
008C:  INCF   36,F
008D:  MOVWF  37
008E:  CALL   03B
008F:  GOTO   087

O primeiro trecho é uma rotina que devolve os caracteres do string, o segundo é um loop para chamar rot1 passando os caracteres um a um.

A chamada rot1 (texto) na linha 19 gera um código bem mais simples, passando o ponteiro inicial da variável texto:

0090:  MOVLW  30
0091:  MOVWF  37
0092:  CALL   03B

O codigo de rot1 sabe tratar os dois casos de passagem de parãmetros.

O incremento das variáveis é bastante direto:

0021:  INCF   27,F
0022:  BTFSC  03.2
0023:  INCF   28,F

0024:  INCF   26,F

O teste de nCont na linha 46 também é simples, porém as instruções de desvio condicional funcionam "pulando" a instrução seguinte quando a condição é verdadeira:

0025:  MOVF   26,W
0026:  SUBLW  64
0027:  BTFSC  03.0
0028:  GOTO   03A
...
003A:  RETLW  00

No codigo acima, subtrai-se 100 (64 em hexa) do conteudo de nCont (que está no endereço 26). Se o resultado for positivo, o código pula por cima do GOTO que leva ao final da rotina.

O processador PIC é um grande desafio para os construtores de compilador. Além da baixa capacidade de processamento e da escassa memória, a arquitetura dificulta o manuseio de constantes (principalmente strings). Apesar disso, existem vários compiladores disponíveis do mercado.

Conclusão

Os sistemas embarcados constituem muita vezes um grande desafio para as linguagens de alto nível. Limitações de memória e arquiteturas não convencional dificultam a implementação de construções corriqueiras, como a passagem de parâmetros e a manipulação de strings constantes.

A linguagem C foi criada com o objetivo (entre outros) de substituir o Assembly e com isto aumentar a produtividade dos programadores de software básico

Em busca deste aumento de produtividade, observa-se de um lado o surgimento de processadores cada vez mais sofisticados e, de outro lado, a engenhosidade dos desenvolvedores de compiladores. O uso de C para sistemas embarcados é hoje corriqueiro. Alguns programadores, inclusive, não tem a mais vaga noção dos "malabarismos" feitos pelo compilador. Os mais experientes, entretanto, aprenderam a obter otimizações no código gerado através de uma seleção mais cuidadosa dos tipos de variáveis e construções e do uso de pragmas e extensões específicas do compilador utilizado.

Ferramentas pessoais