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.
