MATERIAL DESENVOLVIDO PARA TREINAMENTOS DURANTE O LANÇAMENTO DA LINHA FLEXIS DA FREESCALE EM 2009.

Uma breve introdução à linguagem C

Fonte de consulta (https://pt.wikipedia.org/wiki/c_(linguagem_de_programaçao))

C é uma linguagem de programação compilada de propósito geral, estruturada, imperativa, procedural, de alto nível, e padronizada, criada em 1972, por Dennis Ritchie, no AT&T Bell Labs, para desenvolver o sistema operacional UNIX (que foi originalmente escrito em Assembly). A linguagem C é classificada de alto nível pela própria definição desse tipo de linguagem. A programação em linguagens de alto nível tem como característica não ser necessário conhecer o processador, ao contrário das linguagens de baixo nível. As linguagens de baixo nível estão fortemente ligadas ao processador. A linguagem C permite acesso de baixo nível com a utilização de código Assembly no meio do código fonte. Assim, o baixo nível é realizado por Assembly e não C. Desde então, espalhou-se por muitos outros sistemas, e tornou-se uma das linguagens de programação mais usadas, e influenciou muitas outras linguagens, especialmente C++, que foi originalmente desenvolvida como uma extensão para C.

História

Ken Thompson e Dennis Ritchie (da esquerda pra direita), os criadores das linguagens B e C, respectivamente.
Figura 1 –  Ken Thompson e Dennis Ritchie (da esquerda pra direita), os criadores das linguagens B e C, respectivamente.

O desenvolvimento inicial de C, ocorreu no AT&T Bell Labs, entre 1969 e 1973. Deu-se o nome “C” à linguagem, porque muitas de suas características derivaram da linguagem B.

C foi originalmente desenvolvido, para a implementação do sistema UNIX (originalmente escrito em PDP-7 Assembly, por Dennis Ritchie e Ken Thompson). Em 1973, com a adição do tipo struct, C tornou-se poderoso o bastante para a maioria das partes do Kernel do UNIX, serem reescritas em C. Este foi um dos primeiros sistemas que foram implementados em uma linguagem, que não o Assembly, sendo exemplos anteriores, os sistemas: Multics (escrito em PL/I) e TRIPOS (escrito em BCPL). Segundo Ritchie, o período mais criativo ocorreu em 1972.

K&R C

Em 1978, Brian Kernighan e Dennis Ritchie publicaram a primeira edição do livro The C Programming Language. Esse livro, conhecido pelos programadores de C, como “K&R”, serviu durante muitos anos como uma especificação informal da linguagem. A versão da linguagem C que ele descreve é usualmente referida como “K&R C”. A segunda edição do livro, cobriu o padrão posterior, o ANSI C. K&R C introduziu as seguintes características na linguagem:

K&R C é freqüentemente considerado a parte mais básica da linguagem, cujo suporte deve ser assegurado por um compilador C. Durante muitos anos, mesmo após a introdução do padrão ANSI C, K&R C foi considerado o “menor denominador comum”, em que programadores de C se apoiavam quando uma portabilidade máxima era desejada, já que nem todos os compiladores eram atualizados o bastante para suportar o padrão ANSI C. Nos anos que se seguiram à publicação do K&R C, algumas características “não-oficiais” foram adicionadas à linguagem, suportadas por compiladores da AT&T e de outros vendedores. Estas incluíam:

Cálculos de ponto flutuante em precisão simples (no K&R C, os cálculos intermediários eram feitos sempre em double, porque era mais eficiente na máquina onde a primeira implementação do C foi feita).

ANSI C e ISO C

Durante o final da década de 1970, a linguagem C começou a substituir a linguagem BASIC como a linguagem de programação de microcomputadores mais usada. Durante a década de 1980, foi adaptada para uso no PC IBM, e a sua popularidade começou a aumentar significativamente. Ao mesmo tempo, Bjarne Stroustrup, juntamente com outros nos laboratórios Bell, começou a trabalhar num projecto onde se adicionavam construções de linguagens de programação orientada por objectos à linguagem C. A linguagem que eles produziram, chamada C++, é nos dias de hoje a linguagem de programação de aplicações mais comum no sistema operativo Windows da companhia Microsoft; C permanece mais popular no mundo UNIX.

Em 1983, o instituto norte-americano de padrões (ANSI) formou um comitê, X3J11, para estabelecer uma especificação do padrão da linguagem C. Após um processo longo e árduo, o padrão foi completo em 1989 e ratificado como ANSI X3.159-1989 “Programming Language C”. Esta versão da linguagem é frequentemente referida como ANSI C. Em 1990, o padrão ANSI C, após sofrer umas modificações menores, foi adotado pela Organização Internacional de Padrões (ISO) como ISO/IEC 9899:1990, também conhecido como C89 ou C90. Um dos objetivos do processo de padronização ANSI C foi o de produzir um sobreconjunto do K&R C, incorporando muitas das características não-oficiais subsequentemente introduzidas. Entretanto, muitos programas tinham sido escritos e que não compilavam em certas plataformas, ou com um certo compilador, devido ao uso de bibliotecas de funções não-padrão e ao fato de alguns compiladores não aderirem ao ANSI C.

C99

Após o processo da padronização ANSI, as especificações da linguagem C permaneceram relativamente estáticas por algum tempo, enquanto que a linguagem C++ continuou a evoluir. (em 1995, a Normative Amendment 1 criou uma versão nova da linguagem C mas esta versão raramente é tida em conta.) Contudo, o padrão foi submetido a uma revisão nos finais da década de 1990, levando à publicação da norma ISO 9899:1999 em 1999. Este padrão é geralmente referido como “C99”. O padrão foi adoptado como um padrão ANSI em Março de 2000. As novas características do C99 incluem:

O interesse em suportar as características novas de C99 parece depender muito das entidades. Apesar do GCC e vários outros compiladores suportarem grande parte das novas características do C99, os compiladores mantidos pela Microsoft e pela Borland suportam pouquíssimos recursos do C99, e estas duas companhias não parecem estar muito interessadas em adicionar tais funcionalidades, ignorando por completo as normas internacionais. A Microsoft parece preferir dar mais ênfase ao C++.

Visão Geral

C é uma linguagem imperativa e procedural, para implementação de sistemas. Seus pontos de design foram para ele ser compilado, fornecendo acesso de baixo nível à memória e baixos requerimentos do hardware. Também foi desenvolvido para ser uma linguagem de alto nível, para maior reaproveitamento do código. C foi útil para muitas aplicações que foram codificadas originalmente em Assembly.

Essa propriedade não foi acidental; a linguagem C foi criada com o objetivo principal em mente: facilitar a criação de programas extensos com menos erros, recorrendo ao paradigma da programação algorítmica ou procedimental, mas sobrecarregando menos o autor do compilador, cujo trabalho complica-se ao ter de realizar as características complexas da linguagem. Para este fim, a linguagem C possui as seguintes características:

Algumas características úteis, que faltam em C, podem ser encontradas em outras linguagens, que incluem:

Apesar da lista de características úteis que C não possui, ser longa, isso não tem sido um impedimento à sua aceitação, pois isso permite que novos compiladores de C sejam escritos rapidamente para novas plataformas, e também permite que o programador permaneça sempre em controle do que o programa está a fazer. Isto é o que por várias vezes permite o código de C correr de uma forma mais eficiente que muitas outras linguagens. Tipicamente, só código de Assembly “afinado à mão” é que corre mais rapidamente, pois possui um controle completo da máquina, mas avanços na área de compiladores juntamente com uma nova complexidade nos processadores modernos permitiram que a diferença tenha sido rapidamente eliminada.

Uma consequência da aceitação geral da linguagem C é que frequentemente os compiladores, bibliotecas e até intérpretes de outras linguagens de nível maior sejam eles próprios implementados em C.

C tem como ponto forte, a sua eficiência, e é a linguagem de programação preferida para o desenvolvimento de sistemas e softwares de base, apesar de também ser usada para desenvolver programas de computador. É também muito usada no ensino de ciência da computação, mesmo não tendo sido projetada para estudantes e apresentando algumas dificuldades no seu uso. Outra característica importante de C, é sua proximidade do código de máquina, que permite que um projetista seja capaz de fazer algumas previsões de como o software irá se comportar, ao ser executado.

C tem como ponto fraco, a falta de proteção que dá ao programador. Praticamente tudo que se expressa em um programa em C, pode ser executado, como por exemplo, pedir o vigésimo membro de um vetor com apenas dez membros. Os resultados são muitas vezes totalmente inesperados, e os erros, difíceis de encontrar.

C para Desktop X C para embarcados

O primeiro ponto a ser observado nesta comparação é a quantidade de recursos de memória disponíveis para cada um destes ambientes. É óbvio que sistemas embarcados tem menor quantidade de memória disponível e muitas vezes tem velocidade de processamento bem inferior aos “parrudos” computadores de mesa. Poderíamos citar algumas características que devem ser consideradas em ambientes embarcados:

Pode-se concluir que em ambientes embarcados, manipular o hardware é o efeito mais desejado. Não se procura em ambientes embarcados apenas apresentar uma sequência de números ou gráficos coloridos em um display. Muitas vezes o objetivo é colocar um motor para funcionar ou qualquer outra tarefa de tempo real, o que pode necessitar de rotinas de interrupção.

Para que um programa embarcado utilizando linguagem C tenha sucesso ele deve manter o menor código possível e manter a sua eficiência. Para isto, é necessário quebrar alguns regras que são utilizadas na programação focada em desktop.

Em C para desktops, os programadores evitam a todo custo a declaração de variáveis globais. Isto se deve ao fato destas variáveis causarem interação com o hardware do desktop através da BIOS do computador. Já num microcontrolador de 8 bits, você estará escrevendo o programa da BIOS e o sistema operacional do chip! Portanto um acesso global é inevitável!

O uso de ponteiros em C às vezes pode ficar um pouco fora de mão. Ao invés de deixar o compilador simplesmente “tratar” os vários níveis direcionamento dos ponteiros, devemos entender como o compilador manipula os ponteiros e, assim, estruturar o nosso código de para maximizar sua eficiência.

E, finalmente, um dos pecados capitais da programação em linguagem C para desktops deve ser revisto para sistemas embarcados. É difícil admitir isso mas pode haver momentos em que uma instrução GOTO estrategicamente colocada pode resultar em um código mais eficiente.

Em um curso de linguagem C somos orientados do porque de evitar a todo custo a utilização de comandos GOTO ao longo de um código. Isto é porque ele pode rapidamente transformar seu programa em uma espiral “spaghetti”, o que resulta em um software que pode ser extremamente difícil de compreender e depurar. Em linguagem C somos ensinados a repensar o problema sempre que nos deparamos com a tentação de usar um GOTO. Mas um GOTO bem documentado pode certamente ser apropriado em determinadas ocasiões.

ANSI C para μCs de 8 e 32 bits

O C ANSI puro não é sempre a melhor opção para sistemas embarcados, porque:

Deve-se utilizar o C padrão, o tanto quanto for possível, no entanto quando ele interferir com a resolução do problema, não hesite em ignorá-lo.

Outra regra a ser quebrada é o uso do ANSI C puro. Se você não é cuidadoso com a declaração dos tipos de suas variáveis e deixa que o compilador tome conta do gerenciamento destes tipos para você, o resultado certamente será um código maior. Isto ocorre principalmente quando você tenta comparar ou trabalhar com variáveis de tamanhos diferentes. Nestes casos, tudo será convertido para o maior tamanho utilizado na operação.

Oficialmente, o C ANSI puro não entende o hardware. Às vezes precisamos usar truque que não pertencem ao ANSI para obter um software que faça o que desejamos. Interrupções são um destes truques. Não há nenhuma norma ANSI C que explique a maneira de lidar com as interrupções.

O ANSI C é uma linguagem baseada em pilha. Ele passa os parâmetros na pilha e mantém as variáveis locais na pilha. Várias arquiteturas de 8-bit do microcontrolador não fornecer uma CPU com um controle necessário de manipulação de pilha, a fim de produzir códigos em linguagem C eficientes.

Compiladores de linguagem C montados para essas arquiteturas usam uma variedade de truques para criar uma pilha artificialmente por software. O resultado final é um software que pode funcionar, mas que é um pouco confuso e onde provavelmente existem características padrão do ANSI C, tais como a reentrada de funções, que o compilador não irá suportar.

Assim, no mínimo, um bom compilador deveria suportar todas as funcionalidades do ANSI C. Extensões são permitidas, mas algumas exclusões podem causar problemas com a portabilidade do código.

E a capacidade de poder escrever funções reentrantes é uma das grandes habilidade linguagem de programação C. A arquitetura da Freescale para os chips HCS08 e V1 foi projetada especificamente para atender as necessidades dos programas e dos compiladores em linguagem C. Estas arquiteturas fornecem o controle de pilha necessário para produzir um código eficiente em C.

O ANSI C também não tem uma forma padrão de endereçamento de memória para chaveamento em bancos. A arquitetura de 16 bits da Freescale HCS12 suporta nativamente esse tipo de abordagem com seu conjunto de opcodes. Mas outras arquiteturas tem a necessidade de fazer o compilador C passar diversos loops a fim de alcançar todos os recursos que estão localizados outros pontos de outros bancos de memória. A lição a ser aprendida aqui é usar o padrão C, sempre que possível. Mas não se deixe que está seja a única forma, o único caminho a seguir. É necessário compreender as extensões da linguagem que estão disponíveis e saber como usá-las.

Assembly versus C?

Um compilador de linguagem C nunca é mais eficiente do que um bom programador de linguagem Assembly.

Porém é muito mais fácil de escrever bom código em C que pode ser convertido para código um eficiente em linguagem Assembly do que escrever um código em Assembly a mão. A linguagem C deve ser encarada como um meio para um fim e não um fim em si mesma.

A conclusão a ser tirada é que a compreensão do hardware a ser utilizado, dos recursos linguísticos disponíveis pode resultar em um melhor código em C. Isto é especialmente verdadeiro para as pequenas arquiteturas de 8 bits.

Dito isto, é muito mais fácil de compreender e escrever uma rotina em C e, em seguida, convertê-lo em linguagem Assembly do que fazer o inverso.

Mas uma desvantagem é que se temos a capacidade de escrever uma rotina malfeita em linguagem C, não haverá compilador, não importa quão bom ele seja, que possa gerar um código de máquina eficiente.

O C não vai salvar uma má programação.

Variáveis, tipos de dados e operadores em C

Palavras reservadas na linguagem C

A linguagem C é do tipo “case sensitive”. E além disto, algumas palavras são reservadas, sendo seu uso limitado apenas as funções ou comandos que elas executam:

O compilador Code Warrior tem também algumas palavras reservadas, o que veremos ao longo deste treinamento.

Tipos de dados

Basicamente apenas 5 tipos de dados que são utilizadas em linguagem C:

Cada uma destes tipos pode ainda ter os seguintes modificadores:

Detalhes sobre números em ponto flutuante

A base numérica no padrão IEEE754 é a binária. Neste padrão são adotados dois formatos para representação de números: precisão simples e precisão dupla. (Na base binária, um dígito binário é denominado bit e um byte é um conjunto de 8 bits).

Ficou estabelecido que no padrão IEEE754, em precisão simples, um número real seria representado por 32 bits, (4 bytes), sendo que:

Pelo mesmo padrão IEEE754, em precisão dupla, um número real seria representado por 64 bits, (8 bytes), sendo que:

Qualquer valor em ponto flutuante é sempre escrito no seguinte formato:

v = S × M × 2E

Onde:

Como o Code Warrior trata os tipos de dados?

No Code Warrior é possível definir como será tradado cada um dos tipos de dados para o MCU de 8 bits, podendo inclusive fazer com que um dado seja ajustado de modo fora do padrão estabelecido pelo padrão ANSI C. Para isto clique na aba TARGET do projeto, como é mostrado na figura abaixo.

Em seguida dê um duplo clique no target STANDART. Isto fará com que a janela de configurações seja aberta, como mostrado abaixo. Na opção target, selecione o item COMPILER FOR HC08.

Na tela de ajustes clique no botão TYPE SIZE, o que fará com que a janela de ajustes se abra, como mostra a figura abaixo.

Grandes economias no tamanho do código que serão geradas pelo compilador podem ser obtidas através da escolha do tipo correto para cada variável em seu programa:

Declaração de variáveis

É necessário, durante o fluxo do programa, declarar as variáveis que serão utilizadas de acordo com os tipos de dados mostrados anteriormente. Isto pode ser feito das seguintes maneiras:

TIPO nome_da_variável {,outras_variáveis};
unsigned int tempo;

As variáveis podem ser inicializadas com um determinado valor durante a sua declaração:

unsigned int tempo = 100;

Variáveis locais e globais

Dependendo do local onde é declarada a variável, esta pode assumir uma função global ou local:

EXEMPLO 01: PROJETO VARIÁVEIS 1

O programa a seguir (EXEMPLO 01) dá uma demonstração de como as variáveis podem ler declaradas localmente ou globalmente:

Comandos printf e scanf no Code Warrior

Alguns comentários importantes sobre o programa EXEMPLO-01:

O formato do comando printf é:

printf (string, variável);
printf (“O número de tentativas foi %d”, contador);

Onde:

Para que todos os caracteres a serem impressos na janela Terminal I/O não fiquem na mesma linha, são utilizados os caracteres especiais de barra invertida, que são:

Um comando complementar aos printf é o scanf. Enquanto o comando printf, mostrado no exemplo anterior, emula obenvio de dados pela porta serial do software, o scanf emula o recebimento destes dados, também pela porta serial.

Isto também será possível visualizar através do Terminal I/O, acessível no IAR em: View ->
Terminal I/O. Assim como no comando anterior, para que ele funcione também é necessário incluir a biblioteca  padrão stdio.h

O formato do comando scanf é:

scanf (string, variável);
scanf (%d”, contador);

Tendo as mesmas características e controladores vistos no printf.

Comandos prinft e scanf manualmente no Code Warrior

Apesar de não ser reconhecido como um comando nativo do Code Warrior, o manual do simulador deste software traz uma informação referente ao uso do printf, como pode ser visto na figura a seguir.

Trecho do manual do simulador do Code Warrior que cita o comando printf:

Mas aonde este comando pode ser aplicado, já que se você o colocar dentro de seu código ele gerará um erro de compilação? A resposta: dentro da janela de comandos, horas bolas!!!

Note que são utilizadas as mesmas características apresentadas pela linguagem C para a aplicação do comando printf.

Fazendo os comandos prinft e scanf funcionar no Code Warrior

Apesar de não ser reconhecido como um comando nativo do Code Warrior, é possível fazer o comando printf funcionar com os chips de 8 (HSC08) e de 32 (ColdFire) bits. Para tanto é necessário atualizar alguns itens do compilador.

Comandos prinft e scanf para o HSC08

Nos microcontroladores de 8 bits, a função de baixo nível que implementa as entradas e saídas está no arquivo termio.c, geralmente no endereço de instalação do Code Warrior, nas pastas LIB/HC08C/SRC.

Neste arquivo estão as três funções que comandam a entrada e a saída de dados:

TERMIO_GetChar ( ): esta função é utilizada para receber caracteres de um canal de entrada;

TERMIO_PutChar ( ): esta função é utilizada para enviar caracteres para um canal de saída;

TERMIO_Init ( ): esta função inicializa o canal de comunicação.

Para fazer o CodeWarrior enviar e receber dados do terminal IO, é necessário alterar estas três funções, redirecionando os caracteres adequadamente. Após isto feito, é necessário adicionar este arquivo ao seu projeto.

Comandos prinft e scanf para o ColdFire

O primeiro passo para fazer os comandos printf e scanf funcionarem com o ColdFire (32 bits) é editar o arquivo exceptions.c, que está na pasta Startup Code do projeto, como pode ser visto na figura abaixo.

Ao abrir o arquivo exceptions.c, procure a linha que mantém desligado os comandos para o console IO: #define CONSOLE_IO_SUPORT 0, como pode ser visto abaixo.

Altere esta linha para: #define CONSOLE_IO_SUPORT 1, como mostrado abaixo.

Após isto é necessário adicionar o arquivo console_io_cf.c ao seu projeto. A localização deste arquivo pode ser vista na figura abaixo:

Note que este arquivo, o console_io_cf.c, deve ser adicionado na pasta source, juntamente com o arquivo main, como mostra a imagem abaixo:

Quando você for construir seu projeto, clicando do debugging, é necessário que o debugger permita que o comando printf seja executado. Isto é feito através do menu CFMultilinkCyclonePro do debugger, na opção Setup, como pode ser visto abaixo:

Ao clicar em Setup abrirá a janela de configuração, mostrada abaixo. Clique na paleta Debug Options, também mostrada abaixo:

Ai basta clicar na caixa Enable Terminal printf support. Pronto!

Ao executar o EXEMPLO 01, você deve abrir a caixa do terminal IO. Isto é feito clicando no menu Component -> Open, mostrado abaixo:

Dentre as diversas opções de componentes que o debugger oferece, selecione o item Terminal IO, mostrado abaixo:

Basta então executar seu programa, e acompanhar a tela do Terminal IO para ver as mensagens serem impressas, como na figura abaixo:

Declaração de variáveis em MCUs de 8 bits 

Existem três regras básicas para a seleção de tipo de variável que será utilizada em sistemas embarcados com microcontroladores de 8 bits:

• Use o menor tipo possível que permita a você realizar a tarefa desejada.
• Use sempre unsigned type se isto for possível.
• Use casts com as expressões para reduzir os tipos de dados ao menor valor possível.

Obs: uso de cast:

short a=2000;
int b;
b = (int) a; // c-like cast notation
b = int (a); // functional notation 

Além disto é totalmente aconselhável o uso de typedefs para obter tamanhos fixos nas varáveis:

• Haverá alterações de acordo com o compilador e com o sistema.
• Isto torna o código invariante, independente do microcontrolador.
• Utilize sempre que for necessário um número fixo de bits para uma determinada
variável.

Evite, sempre que possível, utilizar os típos básicos da linguagem C em seu código: ‘char’, ‘int’, ‘short’, ‘long’.

EXEMPLO 03: CONSEQUÊNCIA DE USO DE VARIÁVEIS 

Para entender as consequências da correta ou incorreta declaração de tipos das variáveis, vamos executar o EXEMPLO 02:

Cabe aqui uma pergunta: como é possível declarar que a variável VarB é um byte se este não é um tipo válido para a linguagem C?

A resposta está no arquivo MC9S08QE128.h, que acompanha a criação de um projeto e que contém as seguintes definições:

/* Types definition */
typedef unsigned char byte;
typedef unsigned int word;
typedef unsigned long dword;
typedef unsigned long dlong[2];

Ao executarmos este projeto, devemos atentar para a alocação de memória e definição de tipos que acontece, o que pode ser visto nas janelas MEMORY e DATA1 (que indica a locação de área na  memória RAM) do IDE, como pode ser visto na figura abaixo:

Note que a posição 0100 da memória RAM é alocada para a variável VarB, que é um byte (unsigned char) e foi inicializada já na sua declaração com 11 (hexa).

As duas próximas posições da memória RAM foram alocadas para a variável VarC, que é uma word (unsigned int) e foi inicializada já na sua declaração com 2222 (hexa).

As próximas quatro posições da RAM foram alocada para a variável VarD, que é uma dword (unsigned long) e foi inicializada já na sua declaração com 33333333 (hexa).

Por fim, a próxima posições da memória RAM foi alocada para a variável VarA, que foi declarada como unsigned char e foi não inicializada em sua declaração, rebendo então o valor 0 (hexa) em sua posição de memória.

Após algumas execuções do programa, apertando a tecla F10 no simulador, percebemos que as variáveis são incrementadas nas suas respectivas posições de memória RAM, como é possível visualizar na figura da próxima página.

Agora façamos uma alteração neste projeto, criando o EXEMPLO 03: vamos modificar a declaração das variáveis VarB, VarC e VarD, que são GLOBAIS no programa original, transformando-as em variáveis LOCAIS, do seguinte modo:

Algumas surpresas aparecerão quando executarmos o programa e rodarmos o simulador. Primeiro veja o que acontece com a janela DATA1 (que indica a locação de área na memória RAM):

Aonde foram parar as variáveis (agora LOCAIS) VarB, VarC e VarD ??? Se observarmos a janela MEMORY, perceberemos que foi alocado espaço apenas para a variável VarA, a partir do endereço 0100, já que esta é uma variável GLOBAL. E as demais?

Para matar a charada, precisamos observar a janela DATA2 (que indica a manipulação da pilha (stack)):

Voilà!!! Ai estão elas!!!

Se executamos o programa passo a passo (F10 no simulador), veremos que continuamos utilizando posições da memória RAM para armazenar os valores das variáveis, como mostra a figura abaixo, mas agora esta área é pertencente a pilha (stack) e não precisa ficar alocada exclusivamente para a variável, podendo ser utilizada por outra função.

Modificadores de acesso

Além das declarações como global ou local, as variáveis podem receber dois tipos de modificadores de acesso, que especificam a forma como o compilador irá acessar o conteúdo das variáveis:

volatile unsigned int tempo = 100;

Modificadores de armazenamento

Indicam como o compilador irá tratar o armazenamento das variáveis. São quatro tipos:

static volatile unsigned int tempo = 100;

É interessante notar o comportamento das variáveis de acordo com a aplicação dos modificadores e, principalmente, como tirar proveito destes recursos. Podemos dizer que as variáveis tem área de trabalho e local de armazenamento diferenciado, de acordo com suas declarações:

Mas para entender melhor como isto acontece, vamos estudar em detalhes alguns destes modificadores e como eles atuam de modo a criar não apenas um código melhor, mas também um código mais compacto.

Detalhes sobre o modificador de armazenamento static

Quando aplicado a uma variável, o modificador de armazenamento static tem duas funções principais:

Isto é muito aplicável para linguagem C em desktops. Mas para sistemas embarcados, o static terá as seguintes aplicações:

Note que as variáveis com o modificador static são armazenadas globalmente, e não utilizam a pilha (stack).

Um exemplo deste comportamento pode ser notado no programa exemplo mostrado na figura da próxima página.

Funções que foram declaradas como static em um módulo somente podem ser chamadas por outras funções daquele mesmo módulo.

Esta prática de uso de static tem as seguintes características:

A principal vantagem é que desde que o compilador conheça o momento exato em que uma função pode chamar uma função com static, ele pode estrategicamente colocar a função static de modo que seja chamada utilizando uma versão curta de uma instrução call ou jump.

Logo mais a frente há um programa de exemplo para entender melhor este conceito.

Note que a “ExternalFunction” é acessível pelo “main” porque ela foi declarada como “extern”. Porém, as “InternalFunction1” e “InternalFunction2” somente podem ser acessadas por outras funções que estão no módulo “stuff.c”.

Utilizar o modificador static dará ao compilador a oportunidade de aplicar uma técnica de otimização que definirá como e quando estas funções serão chamadas. Isto é reconhecidamente mais eficiente em arquiteturas como a do HSC08.

Detalhes sobre o modificador de acesso volatile

Uma variável que recebe um modificador de acesso volatile passa a poder ter seu valor alterado fora do fluxo normal do programa. Isto pode acontecer, em sistemas embarcados, de duas maneiras:

É considerado uma excelente prática fazer a declaração de todos os registradores que trabalham com periféricos como volatile.

Uma técnica comum para fazer isto é mostrada abaixo:

volatile unsigned char PTAD @0x0000
volatile PTADSTR _PTAD @0x0000 (CodeWarrior Stationary)

Outra técnica, que garante um pouco mais de portabilidade ao código é a seguinte:

#define reg_ptad (*(volatile Tptad*) (0x0000)

O exemplo abaixo demonstra um potencial de exigência de hardware para armazenar o mesmo valor para o mesmo local. Mostra também uma maneira de realizar uma “leitura simulada” de um registrador de status, algo que é exigido por muitos periféricos em suas rotinas de interrupção de serviço.

Sem o modificador volatile, essas operações de hardware necessitam ser otimizadas pelo compilador. Usando o modificador volatile, o problema é corrigido.

Detalhes sobre o modificador de acesso const

É seguro supor que uma variável que receba o modificador de acesso const, passe a ser reconhecida como somente leitura. Porém, alguns compiladores criam uma variável real na RAM para armazenar a variável do tipo const.

Assim, após a inicialização do sistema, o valor da variável de apenas leitura é copiado para a RAM. Em sistemas que tenham quantidade limitada de memória RAM, esta pode ser uma pena significativa de capacidade apenas pelo fato de ter colocado as variáveis como o modificador const. Compiladores para sistemas embarcados, como é o caso do Code Warrior, as variáveis const são armazenadas em ROM (ou Flash).

No entanto, apesar da característica de somente leitura, ela ainda é uma variável é acessada pelo programa, como tal, embora o compilador tente proteger as definições const de serem escritas inadvertidamente. Cada variável const deve ser declarada com um valor de inicialização.

Detalhes sobre os modificadores em conjunto

Será que uma variável pode ser tratada como const e volatile ao mesmo tempo? A resposta é sim!

Mas será que isto faz sentido para sistemas embarcados? Sim! E isto se aplica a qualquer local da memória que pode mudar inesperadamente (volatile) mas que é de somente leitura (const).

O exemplo mais óbvio do que estamos falando é o registrador SCI1S1, que controla o estado da máquina SCI, mostrado abaixo:

Este registrador contém flags para sinalizar condições como: TX vazio (TDRE), TX terminado (TC), RX cheio (RDRF), entre outros. Este é um registro volátil, uma vez que estas flags tendem a mudar inesperadamente, dependendo do estado da comunicação serial, e também são de condição somente leitura, pois as flags não podem ser gravados diretamente pelo programa, eles respondem apenas ao estado do módulo SCI.

A melhor declaração de registrar este status é:

const volatile unsigned char SCI1S1 @0x0024 /* SCI1 Status Register 1 */

Ou num formato que torna o código mais portável:

#define reg_sci1s1 (*(const volatile tSCI1S1*) (@0x0024) /* SCI1 Status Register 1 */

Operadores em C

Operador atribuição

É utilizado para atribuir um determinado valor a uma variável. Seu símbolo é o “ = ” e alguns exemplos de atribuição são mostrados a seguir:

unsigned int x, y;
x = 10;
y = x;

A atribuição é sempre avaliada da direita para a esquerda. Isto significa que ao final deste programa o valor da variável y será igual a 10.

Operadores aritméticos

Indica ao compilador que ele deve fazer determinada operação aritmética entre duas variáveis. São os seguintes:

O EXEMPLO 04 mostra a consequência de uso dos operadores aritméticos:

Operadores relacionais

São utilizados em testes condicionais para determinar a relação existente entre os dados:

int x = 10;

x > 8;     //verdadeiro
x == 10;   //verdadeiro
X < 100;   //verdadeiro
x > 20;    //falso
x != 10;   //falso
x >= 10;   //verdadeiro
x <= 10;   //verdadeiro

Operadores lógicos

São utilizados para fazer conjunções, disjunções ou negações entre elementos durante um teste condicional.

Operadores lógicos bit a bit

Operadores de memória ou de ponteiro

Os ponteiros serão tratados em detalhes no estudo dos dados avançados. É possível dizer que este é um dos pilares fundamentais da linguagem C e portanto gastaremos algumas horas do nosso treinamento para explicá-los em detalhes. Por hora, vamos apenas conhecer quais são os símbolos utilizados como operadores de memória, ou de ponteiros, e suas funcionalidades. As aplicações serão desenvolvidas mais a frente.

São dois os símbolos:

endereço_a = &a;

teremos que a variável “endereço_a” conterá o endereço em que está armazenada a variável a.

a = *endereço_a;

teremos que o valor armazenado no local apontado pela variável “endereço_a” seja atribuído à variável a.

Outros operadores

Outros operadores, que não se encaixam em nenhum dos tipos citados anteriormente, mas que podem ser utilizados em linguagem C são:

variável = expressão1 ? expressão2 : expressão3;

Ele funciona da seguinte maneira: avalie a expressão1. Se ela for verdadeira, atribua a
variável o valor da expressão2. Caso a expressão1 seja falsa, então a variável recebe o valor da expressão3.

variável = (expressão1 , expressão2 , expressão3);

Uma aplicação, como a seguir, resulta em y = 5.

y = ( x = 0 , x + 5 );
(tipo) expressão;

Onde tipo é qualquer um dos tipos de dados permitidos pela linguagem C e expressão é a que se quer que resulte no tipo especificado. Por exemplo: para realizar um cálculo e obrigar que o resultado seja apresentado em ponto flutuante, pode-se utilizar o seguinte cast:

unsigned int x, y;
x = 10;
y = (float) x / 2;

sizeof -> Retorna o tamanho de uma variável: para ter um melhor controle da quantidade de memória utilizada, pode-se aplicar o operador sizeof a qualquer variável. Isto fará retornar qual o tamanho que esta variável ocupa em bytes.

EXERCÍCIOS: Operadores,tipos e variáveis

EXERCÍCIO 01: Exemplos em 32 bits

Os três exemplos tratados anteriormente funcionam no MCU de 8 bits (MC9S08QE128). Reescreva os três exemplos, criando três novos projetos, de modo que eles sejam executados no MCU de 32 bits (MCF51QE128).

EXERCÍCIO 02: Calculadora básica

Escreva um programa em que encontre o valor de x e y assumindo que seus valores são calculados da seguinte maneira:

ax + by = c
dx + ey = f

Assuma, em seu programa, que a, b, c, d, e e f são variáveis globais e que o resultado esperado para x e y seja armazenado em variáveis locais.

EXERCÍCIO 03: Calculadora de Idade – I

Escreva um programa em que o usuário entre com a data de nascimento no formato na seguinte sequencia:

Na sequencia, o usuário deve entrar com a data de hoje, fornecendo os valores de dia, mês e ano no mesmo formato.

O programa deve, então, calcular a idade do usuário, informando a idade em ANOS, cujo valor deve ser um inteiro.

EXERCÍCIO 04: Calculadora de Idade – II

Modifique o programa anterior de modo que seja mostrado na tela também o valor da idade em DIAS e MESES.

Trabalhando com bits

Em sistemas embarcados é essencial tem o acesso habilitado para que seja feita a modificação de apenas um bit, ou um conjunto de bits, dentro de um byte, dado um determinado endereço, que pode ser de uma porta do MCU.

A flexibilidade oferecida pela linguagem C permite realizar este acesso de diversas maneiras diferentes, como por exemplo:

A chave para uma eficiente manipulação de bits está no modo em como estes bits foram declarados. Veja o exemplo abaixo, retirado dos arquivos do Code Warrior:

CodeWarrior Stationary:
/*** PTAD - Port A Data Register ***/
typedef union {
byte Byte;
struct {
byte PTAD0 :1; /* Port A Data Register Bit 0 */
byte PTAD1 :1; /* Port A Data Register Bit 1 */
byte PTAD2 :1; /* Port A Data Register Bit 2 */
byte PTAD3 :1; /* Port A Data Register Bit 3 */
byte PTAD4 :1; /* Port A Data Register Bit 4 */
byte PTAD5 :1; /* Port A Data Register Bit 5 */
byte PTAD6 :1; /* Port A Data Register Bit 6 */
byte PTAD7 :1; /* Port A Data Register Bit 7 */
} Bits;
struct {
byte PTAD :8;
} MergedBits;
} PTADSTR;
extern volatile PTADSTR _PTAD @0x00000000;
#define PTAD _PTAD.Byte
#define PTAD_PTAD0 _PTAD.Bits.PTAD0
#define PTAD_PTAD1 _PTAD.Bits.PTAD1
#define PTAD_PTAD2 _PTAD.Bits.PTAD2
#define PTAD_PTAD3 _PTAD.Bits.PTAD3
#define PTAD_PTAD4 _PTAD.Bits.PTAD4
#define PTAD_PTAD5 _PTAD.Bits.PTAD5
#define PTAD_PTAD6 _PTAD.Bits.PTAD6
#define PTAD_PTAD7 _PTAD.Bits.PTAD7
#define PTAD_PTAD _PTAD.MergedBits.PTAD

Com este tipo de declaração é possível fazer uma excelente manipulação de bits durante o seu código, como sugere o exemplo abaixo:

#define dTurnOnRedLED (PTAD_PTAD0 = 0)
#define dTurnOffRedLED (PTAD_PTAD0 = 1)
{
dTurnOnRedLED;
}

Que bit representa cada pino do μC?

A primeira observação é que grande parte dos pinos de um MCU sempre é compartilhado entre diversos periféricos, como temporizadores sistemas de comunicação serial, interrupções de teclado, etc. Nas famílias de MCUs da Freescale que estudamos neste treinamento, todos os periféricos tem prioridade de acesso aos pinos em relação as funções de I/O. Isto significa dizer que quando um periférico está habilitado, a função de I/O daquele pino está desabilitada, automaticamente.

Quando uma função analógica compartilhada é habilitada em um pino, tanto os buffers de entrada e como os de saída são desabilitados. Um valor 0 é lido por qualquer bit de dados da porta onde o bit é uma entrada (PTxDDn = 0) e buffer de entrada está desativado.

Em geral, quando um pino é compartilhado tanto com a função digital e como com uma função analógica, a função analógica tem prioridade de tal forma que, se ambas as funções analógicas e digitais estão habilitados, a função análogica controla a pino.

Registradores que controlam os I/Os

REGISTRADOR DE DADOS (Port x Data Register PTxDn) Cada bit colocado em um pino do microcontrolador tem seu valor refletido neste registrador. Isto ocorre quando a porta está configurada para entrada, sendo válida as seguintes informações:

REGISTRADOR DE DIREÇÃO (Port x Data Direction Registers PTxDDn)

Este registrador indicará se um pino de I/O será utilizado como entrada ou saída, de acordo com a seguinte configuração:

REGISTRADORES DE PULL UP (Port x Pull up Resistor Enable Registers PTxPEn)

Resistores programáveis de pull-up estão disponíveis nos pinos de todos os terminais de I/O, cuja habilitação ou não é feita do seguinte modo:

REGISTRADORES DE CONTROLE DE SLEW RATE (Port x Slew Rate Enable Registers
PTxSEn)

O controle de slew rate pode ser ativado para cada pino das portas do MCU, definindo qual bits se deseja controlar através deste registrador (PTxSEn). Quando ativado, o controle de slew rate limita a velocidade a que uma saída pode ter uma possível transição, a fim de reduzir as emissões EMC. O controle de slew rate não tem nenhum efeito sobre os pinos que são configurados como entradas e pode ser configurado do seguinte modo:

REGISTRADORES DE CONTROLE DE DRIVE STRENGTH (Port x Drive Strength Enable Registers PTxDSn)

Um pino de saída pode ser selecionado para ter um aumento na corrente de saída, o que é definindo pelo bit correspondente no registrador (PTxDSn). Quando este aumento é selecionado, um pino será capaz de fornecer ou receber uma quantidade de corrente maior. Mesmo que cada pino de I/O possa ser selecionado como de alta capacidade de corrente, o usuário deve assegurar que a fonte de corrente total e seus limites para o MCU não sejam ultrapassados. Este registrador de seleção destina-se a afetar o comportamento do DC do pino de I/O. No entanto é bom lembrar que o comportamento AC também é afetado. Este aumento na corrente permitirá que um pino tenha uma maior movimentação de carga com a mesma velocidade de comutação que tem um pino como uma baixa comutação de carga. Devido a isso, as emissões EMC podem ser afetadas quando esta função está habilitada em um pino.

Bit = 0: O controle de drive strength está desabilitado.
Bit = 1: O controle de drive strength está habilitado.

Características DC do MC9S08QE128 (8 bits)

Os efeitos da utilização do drive strength pode ser notado nos seguintes parâmetros:

Outros parâmetros de corrente contínua, afetados pela utilização do drive strength utilização a classificação que a Freescale faz em cada uma das etapas de teste, produção e utilização dos seus microcontroladores, de acordo com a tabela a seguir:

Características DC do MCF51QE128 (32 bits)

Declarações de controle e repetição

As declarações de controle e repetição são algumas das estruturas mais importantes da linguagem C. Elas é que garantem um fluxo de programa capaz de tomar decisões e desviar o programa de acordo com determinadas regras. Veremos neste treinamento estas estruturas, definidas pelo padrão ANSI C e como utilizá-las.

Falso e verdadeiro em linguagem C

Diversos destes comandos de controle e repetição acontecem apenas após um teste condicional, onde é verificado o status de VERDADEIRO ou FALSO de uma variável. É necessário entender como o C observa esta condição para compreender perfeitamente como as decisões são tomadas:

Comando de seleção condicional if

if (condição) comandoA; {else comandoB}

if (condição)
{
comandoA;
comandoB;
comandoC;
}
else
{
comandoD;
comandoE;
comandoF;
}

EXEMPLOS 04 e 05: uso da seleção condicional if

Outro exemplo de utilização da função if está no programa EXEMPLO 05:

Note que este programa chama algumas funções de configuração, mostradas abaixo:

If – Else – If: comandos aninhados

if (condição1) comandoA;
   else if (condição2) comandoB;
      else if (condição3) comandoC;

Uma alternativa: o comando ?

variável = expressão1 ? expressão2 : expressão3;

Ele funciona da seguinte maneira: avalie a expressão1. Se ela for verdadeira, atribua a variável o valor da expressão2. Caso a expressão1 seja falsa, então a variável recebe o valor da expressão3.

Comando de seleção condicional switch

O comando switch difere do comando if porque somente pode testar igualdades. O comando if pode avaliar uma expressão lógica ou relacional.

switch (variável)
{
case constante1:
comandoA;
comandoB;
....
break;
case constante2:
comandoC;
....
break;
....
default:
comandoE;
comandoF;
....
}

EXEMPLO 07e 08: uso da seleção condicional switch case

Veja um exemplo de utilização da função switch case no programa EXEMPLO 06, que contém os comandos printf e scanf:

Veja outro exemplo de utilização da função switch case no programa EXEMPLO 07, que não contém os comandos printf e scanf:

Comando de iteração (laço) for

for (inicialização ; condição ; incremento) comando;
for (inicialização ; condição ; incremento)
{
comandoA;
comandoB;
....
}

EXEMPLOS 08 e 09: uso da iteração (laço) for

Veja no programa EXEMPLO 08 como utilizar o laço for, com o uso do comando printf:

Agora teste o programa EXEMPLO 09 como utilizar o laço for, sem o uso do printf:

Como criar um laço infinito com o for?

for ( ; ; );

Cláusula break no comando for

Como sair de um laço infinito? Execute um comando break dentro do laço. Ele retira o programa da execução infinita. É bom lembrar que quando um comando break é encontrado em qualquer lugar do corpo for, ele causa seu término imediato.

O controle do programa passará então imediatamente para o código que segue o loop.4

Cláusula continue no comando for

Qual o efeito de trocar uma cláusula break por uma cláusula continue? Algumas vezes torna-se necessário “saltar” uma parte do programa. Para isto utilizamos o continue. Isto faz com que o programa:

Comando de iteração (laço) while

while (condição) comando;

while (condição)
{
comandoA;
comandoB;
....
}

EXEMPLO 10: funcionamento do laço while

Veja no programa EXEMPLO 10 como utilizar o laço while:

Como criar um laço infinito com o while?

while (1)
{
......
}

Comando de iteração (laço) do-while

do comando while (condição);

do
{
comandoA;
comandoB;
....
} while (condição);

EXEMPLO 11: funcionamento do laço do-while

Veja no programa EXEMPLO 11 como utilizar o laço do-while:

Usar um laço while (enquanto) ou um laço for (para)?

Os dois laços, while e for, tem várias similaridades. Na grande maioria das situações o mesmo problema poderá ser resolvido tanto com um tipo de laço quanto com o outro. Para uma melhor compreensão do que estamos dizendo, vamos fazer uma equivalência entre os parâmetros que são ajustados para fazer rodar um laço for e os mesmos parâmetros para fazer exatamente o mesmo efeito, mas com um laço while.

for ( A ; B ; C) printf (“%d\t”,i);
A;

while (B)
{
printf (“%d\t”,i);
C;
}

A escolha por um ou outro modelo dependerá apenas do estilo do programador. A forma for é mais compacta. Já a forma while permite uma visualização mais fácil de todas as partes envolvidas. Enfim: ambos geram os mesmos efeitos na linguagem C pura e podem ser considerados iguais pelo programador.

Já a sua aplicação em microcontroladores deve ser analisada com cuidado. Dependendo do compilador e da forma como o laço é gerado, pode ser obtido programas em Assembly maiores ou menores para executar a mesma tarefa. A velocidade com que o programa executa o laço também é influenciada pelo estilo de conversão que o compilador fará para cada um deles.

Um excelente exercício é modificar as opções de otimização do IAR (por velocidade ou por tamanho de código) e compilar programas com resultados de saída iguais mas que façam uso de laços diferentes, observando o comportamento do código Assembly gerado pelo compilador.

Todos estes conceitos juntos? Exemplo 12

Veja no programa EXEMPLO 12 todos os conceitos deste capítulo reunidos em um único programa, que usa os comandos printf e scanf:

Comando de desvio (salto) goto

goto label;

EXERCÍCIOS: Declarações de controle e repetição

Para a série de exercícios a seguir, faremos a manipulação de bits conectados às portas dos microcontroladores. Para que tenhamos sucesso nesta empreitada, é necessário primeiramente entender como as portas podem ser manipuladas. 

Identificação de pinos na DEMOQE128

Identifique no diagrama elétrico da DEMOQE128, mostrado no item 7, as seguintes conexões, envolvendo pinos dos MCUs MC9S08QE128 e MCF51QE128 e seus respectivos hardwares externos:

a) Botão S1 -> pino PTA2;

b) Botão S2 -> pino PTA3;

c) LED1 -> pino PTC0;

d) LED2 -> pino PTC1;

e) LED3 -> pino PTC2;

f) LED4 -> pino PTC3.

Com estas informações escrevam programas em linguagem C de modo que aconteçam as operações solicitadas nos itens a seguir.

EXERCÍCIO 06: 1 botão e 1 led

Ao pressionar o botão S1 deve acender o LED1. Se o botão não estiver pressionado, o
LED1 deve se manter apagado.

EXERCÍCIO 07: 2 botões e 2 leds

Ao pressionar qualquer um dos quatro botões (S1, S2, S3 e S4), os LEDS correspondentes devem ser acesos (LED1, LED2, LED3 e LED4). Se qualquer um dos botões não stiverem
pressionados, os LEDS correspondentes devem se manter apagados.

EXERCÍCIO 08: Contador binário

Escreva um programa que a cada pressionar do botão S1 deve ser incrementada a quantidade de LEDs a serem acesas na porta PTC. Deste modo, após pressionar o botão
S1 por 63 vezes todos os LEDS da porta PTC devem estar acesos. No 64 pressionar de botão, todos os LEDs se apagam e a contagem deve se reiniciar.

EXERCÍCIO 09: 2 botões e 2 leds com temporização simples

Ao pressionar o botão S2 deve apagar o LED1, o que deve acontecer por alguns milissegundos (tempo suficiente para perceber a retenção da informação). Se o botão não
estiver pressionado, o LED1 deve se manter aceso. Ao mesmo tempo, se o botão S1 estiver pressionado o LED2 deve ficar apagado, o que também deve acontecer por alguns milissegundos (tempo suficiente para perceber a retenção da informação). Se o botão não
estiver pressionado, o LED2 deve se manter aceso.

EXERCÍCIO 10: Calculadora de funções

Escreva um programa que encontre o valor de f(x) para a seguinte equação:

f(x) = X 2 – 3X + 2

Fazendo X variar de 0 a 3 em passos de 0,1.

EXERCÍCIO 11: botões e leds com o ColdFire V1

Refaça os exercícios 06 a 09 de modo que eles possam funcionar também no chip  MCF51QE128, de 32 bits.