Professional Documents
Culture Documents
Ismael Potolicchio
22 de marzo de 2018
Técnicas Digitales II
Departamento de Electrotecnia
Facultad de Ingenierı́a
Índice
1. Introducción al Lenguaje C 1
1.1. ¿Por qué utilizarlo en microcontroladores? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2. Primer programa en C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
2. El preprocesador 2
4. Operadores 3
4.1. Operadores aritméticos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
4.2. Operadores de asignación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
4.3. Operadores de incremento y decremento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
4.4. Operadores relacionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
4.5. Operadores lógicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
4.6. Operadores de manejo de bits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
4.7. Prioridad de ejecución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
4.8. Conversión de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
5. Estructuras de control 8
5.1. Estructuras condicionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
5.1.1. Estructura alternativa if-else . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
5.1.2. Estructura de selección switch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
5.2. Estructuras repetitivas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
5.2.1. Estructura while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
5.2.2. Estructura do-while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
5.2.3. Estructura for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
5.3. Transferencias o saltos incondicionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
5.3.1. Break . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
5.3.2. Continue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
5.3.3. Go to . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
5.3.4. Return . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
6. Funciones 12
6.1. Prototipos de funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2
1. Introducción al Lenguaje C
El lenguaje de programación C fue creado por Dennis Ritchie en 1972 en Bell Telephone Laboratories, con
el objetivo de reescribir un sistema operativo, el UNIX, en un lenguaje de alto nivel, para poder adaptarlo
(portarlo) a diferentes arquitecturas. Por este motivo sus creadores se propusieron metas de diseño especiales,
tales como:
Posibilidad de acceder al “bajo nivel” (poder utilizar todos los recursos del hardware).
La primera definición oficial del lenguaje fue dada en 1978 por Brian Kernighan y Dennis Ritchie en su libro
“El lenguaje de programación C”. Este lenguaje fue llamado “C K&R”. En 1983 se creó el comité ANSI que
en 1988 estableció el standard ANSI C, con algunas reformas sobre el C K&R. Simultáneamente Kernighan y
Ritchie publicaron la segunda edición de su libro, describiendo la mayor parte de las caracterı́sticas del ANSI
C. Actualmente existen implementaciones de C para todas las arquitecturas y sistemas operativos, y es uno de
los lenguajes más utilizado para la programación de sistemas.
1
Obviando las dos primeras lı́neas del programa anterior, se puede observar que el mismo comienza con la
función main () cuyo cuerpo principal es un conjunto de sentencias delimitadas por dos llaves, una inmediata-
mente después de la declaración main () “{”, y otra que finaliza el listado “}”. Dentro de este listado existe
la declaración de dos variables en punto flotante: radio y area. Esta sentencia, como todas las del lenguaje C,
finaliza con “;”. Este sı́mbolo le indica al compilador el final de una lı́nea de código y el inicio de la siguiente.
Luego, viene una lı́nea de comentarios. Estos sirven para documentar el código, explicar que hace alguna sen-
tencia, etc. Existen los comentarios de lı́nea simple, los cuales comienzan con “//”, como ası́ también los de
múltiples lı́neas, que comienzan con “/*” y finalizan con “*/”. Posteriormente se realiza la asignación de un
valor a la variable radio para luego calcular el valor del área de un cı́rculo. Finalmente, se imprime por pantalla
el valor del área, utilizando la función printf().
2. El preprocesador
El compilador C tiene un componente auxiliar llamado preprocesador, que actúa en la primera etapa del
proceso de compilación. Su misión es buscar en el texto del programa entregado al compilador ciertas órdenes,
llamadas directivas, que le indican que debe realizar alguna tarea a nivel de texto. Esto puede significar incluir
archivos headers, reemplazar cadenas de caracteres, entre otras. Una vez realizadas estas tareas, el preprocesador
entrega el texto resultante al resto de las etapas de compilación.Se listan a continuación las directivas más usadas.
#define: Si una misma constante o expresión aparece repetida varias veces en un texto, y es posible que
su valor deba cambiarse en algún momento, es muy conveniente definirla con un sı́mbolo y especificar su
valor sólo una vez. Para esto se puede utilizar la directiva #define. En el ejemplo de la figura 1 se utiliza
para definir el valor de PI.
#include: Se utiliza para incluir el texto de un archivo dentro de otro. Es decir, el preprocesador sustituye
la lı́nea #include header1, por el contenido del archivo header1. En el ejemplo de la figura 1, se utiliza
esta directiva para incluir el archivo stdio.h, el cual nos permite luego hacer uso de la función printf().
Las directivas pueden aparecer en cualquier lugar del programa, pero sus efectos se ponen en vigor recién a
partir del punto del programa en que aparecen y hasta el final del programa. Es decir, un sı́mbolo sólo puede
utilizarse después de la aparición de la directiva que la define, y no antes. Las directivas para incluir archivos
suelen darse al principio de los programas, porque en general se desea que su efecto alcance a todo el archivo
en el cual se están incluyendo. Por esta razón los archivos preparados para ser incluı́dos se llaman archivos de
cabecera o headers.
El primer carácter debe ser alguno de los siguientes: “A” a “Z” o“a” a “z” o “ ”.
Los siguientes caracteres pueden ser: “A” a “Z” o“a” a “z” o “1” a “9” o “ ”.
2
auto double int struct
break else long switch
case enum register typedef
char extern return union
const float short unsigned
continue for signed void
default goto sizeof volatile
do if static while
En la tabla 2 se observan los tipos de datos fundamentales que se pueden asignar a las variables y su tamaño
en bits.
A su vez, al declarar una variable, se puede agregar un cualificador, el cual es un modificador del tipo
de dato. Este cualificador cambia el rango de la variable y la signa. En la tabla 3 se muestran las posibles
combinaciones entre tipos de datos fundamentales y cualificadores. Las variables definidas como punto flotante
no llevan cualificadores.
Siempre debe declararse una variable antes de utilizarla. La forma de hacerlo es asignarle un tipo y un
identificador, tal como se muestra a continuación:
4. Operadores
Un operador es un sı́mbolo que denota una operación aritmética, lógica u otra operación particular. Cada
operación se realiza sobre uno o más operandos y se caracteriza por la prioridad de ejecución respecto de otra
3
operación y por la asociatividad. Los operadores se clasifican según el tipo de operación que realizan y sobre
qué actúan.
OPERADOR OPERACIÓN
+ adición
- resta
* multiplicación
/ división
% resto de la división
4
OPERADOR EJEMPLO DESCRIPCIÓN
++a Variable “a” es incrementada en 1 y luego usada
++
a++ Variable “a” es usada y luego incrementada por 1
- -b Variable “b” es decrementada en 1 y luego usada
--
b- - Variable “b” es usada y luego incrementada por 1
char a, b, c;
a = b = 5;
c = 1 + a++; // c = 6
b = ++c + a; // b = 7 + 6 = 13
char prop;
char var = 5;
prop = var < 10; // Expresión es evaluada como verdadera, prop = 1
5
4.6. Operadores de manejo de bits
A diferencia de las operaciones lógicas que se realizan sobre expresiones, las operaciones de manejo de bits
se realizan sobre los bits de un operando. Se enumeran en la siguiente tabla:
Se debe destacar que el resultado de la operación de desplazamiento a la derecha depende del signo de la
variable. En caso de que el operando se aplique a una variable sin signo o positiva, se introducirán los ceros en
el espacio vacı́o creado por desplazamiento. Si se aplica a una variable con signo negativo, se introducirá un 1
para mantener el signo correcto de la variable.
PRIORIDAD OPERADORES
Alta ()[]− > .
! + + − − ∗ P untero&P untero
∗/ %
+−
<<>>
<<=>>=
==! =
&
ˆ
|
&&
||
?:
Baja =+=−=∗=/=/=
Los operadores que se encuentran en un renglón superior de la tabla tienen prioridad de ejecución sobre los
que se encuentran en un renglón inferior. En una expresión compleja, formada por varias subexpresiones conec-
tadas por operadores, es peligroso hacer depender el resultado del orden de evaluación de las subexpresiones. Si
los operadores tienen la misma prioridad, la evaluación se hace de izquierda a derecha, pero en caso contrario
el orden de evaluación no queda definido. Por ejemplo, en la expresión:
v = w * x / ++y + z / y;
Se puede contar con que primeramente se ejecutará w *x y sólo entonces x / ++y, porque los operadores de
multiplicación y división tienen la misma prioridad, pero no se puede asegurar que w * x / ++y sea evaluado
antes o después de z / y, lo que hace que el resultado de esta expresión sea indefinido en C. El remedio es
secuencializar la ejecución dividiendo las expresiones en sentencias:
6
a = w * x / ++y;
b = a + z / y;
Lo mostrado en el código anterior es una buena práctica de programación. Se recomienda separar las sucesivas
aplicaciones de operadores en varias lı́neas de código, con el objetivo de establecer de manera clara cuál es el
orden de prioridad a realizar, además de evitar realizar operaciones no deseadas por la asociativadad dada por
el lenguaje C. Otro ejemplo de un resultado erróneo se muestra a continuación:
char a, b, res;
a = 10;
b = 100;
res = a*(a + b); // resultado = 1100
res = a*a + b; // resultado = 200
Mientras que la expresión que da como resultado 1100 es la correcta, la que da 200 no lo es. La solución más
sencilla es la utilización de paréntesis para agrupar operaciones, como ya se mencionó
char a;
float b,res;
...
res=a+b; //a se convierte temporalmente a float para realizar la suma
Sin embargo, debe procurarse definir de manera correcta el tipo de dato de la variable en donde se almacenará
el resultado.
char a,res;
float b;
... //a se convierte temporalmente a float para realizar la suma
res=a+b; //pero como res es de tipo char, el resultado se trunca.
Observese lo que sucede en la porción de código anterior. Si b=20.5, y a=11, res terminará valiendo 31, lo
que es similar a aplicar una función “parte entera” implı́citamente. Pero si la parte entera de b excede el rango
del tipo de dato de a, por ejemplo si b=534.53, el resultado en res no tendrá lógica aparente. Esto es ası́ dado
que el lenguaje C conserva los dı́gitos menos significativos durante este tipo de conversión.
Este tipo de conversión o promoción se denomina implı́cita o forzada. Existe otro tipo de conversión, la explı́cita.
Esta es realizada cuando se hace referencia a la misma mediante una operación dentro del código. Para realizarla
se utiliza el operador “cast”:
7
char a,b;
float res;
...
res=(float) a/b; //se aplica cast a ‘‘a’’ antes de realizar la división
Para evitar caer en resultados erróneos, deben tenerse en cuenta las siguientes pautas:
Ante una determinada operación, es primordial conocer qué tipo de dato tendrá el resultado de la misma.
Entonces, la declaración de la variable resultado debe ser congruente con esto.
Es tarea de la persona que escribe el código realizar las verificaciones pertinenentes cuando sea posible,
dado que el compilador no generará un error de compilación ante una conversión o promoción implı́cita.
En determinados casos puede realizar una advertencia, pero el código compilará de manera satisfactoria.
En el caso que sea imposible hacer que dos operandos sean del mismo tipo, debe aplicarse un cast para
promover el operando de menor prioridad, al de mayor prioridad.
5. Estructuras de control
5.1. Estructuras condicionales
Las condiciones dentro de un código permiten ejecutar una o varias lı́neas de programa dependiendo de la
validez de una expresión. En otras palabras, las condiciones se utilizan para la toma de decisiones del tipo “Si se
cumple la condición (...), se debe hacer (...). De lo contrario, si la condición no se cumple, se debe hacer (...)”.
En el caso del lenguaje C, las estructuras condicionales no difieren de las incluı́das en Fortran, Basic, Pascal o
Matlab.
if(expresión)
{
sentencia;
sentencia;
}
Si el resultado de la expresión encerrada entre paréntesis es verdadero, las sentencias encerradas entre llaves
se realizan y el programa continúa con la ejecución. Si el resultado de la expresión es falso, las sentencias dentro
de las llaves no se realizan y el programa continúa inmediatamente con la ejecución. Si no se colocan las llaves,
un resultado verdadero en la evaluación de la expresión hace que se ejecute solo la sentencia que se encuentra
por debajo del if. Si no se cumple la expresión, esto es, da un resultado falso, se salta la primer lı́nea de código
por debajo del if y se continúa con la ejecución del programa.
if(expresión)
{
sentencia;
sentencia;
}
else
{
sentencia;
sentencia;
}
8
Cuando se agrega la expresión else, se ejecutan las sentencias dentro de las llaves que prosiguen al else en
caso de que la expresión evaluada sea falsa. Si no se colocan las llaves, la ejecución se sucede de la misma manera
que la expuesta anteriormente para el if.
A diferencia de la estructura if-else que selecciona entre dos opciones en el programa, la estructura switch
permite elegir entre varias opciones. La misma se muestra a continuación:
case constante2:
operación2 // El grupo de operadores se ejecuta si
... // el selector y la constante2 son equivalentes
break;
...
default:
operación_esperada // El grupo de operadores que se ejecuta si
... // ninguna constante equivale al selector
break;
}
La estructura switch se ejecuta de la siguiente manera: primero se evalúa la variable selector y se compara
con el valor de constante1. Si coinciden, las sentencias que pertenecen a ese bloque se ejecutan hasta llegar a
la palabra clave break o hasta el final de la operación switch. Si no coinciden, el selector se compara con la
constante2. Si coinciden, las sentencias que pertenecen a ese bloque se ejecutan hasta llegar a la palabra clave
break. Si el selector no coincide con ninguna constante, se ejecutarán las operaciones que siguen al operador
default. También es posible comparar una expresión con un grupo de constantes. Si coincide con alguna de ellas,
se ejecutarán las operaciones apropiadas:
La sentencia break es muy importante, ya que el comportamiento normal de una estructura switch es eje-
cutarlo todo desde la coincidencia de la variable selector con el case correspondiente, hasta el final. Por ello,
si no se quiere que se ejecute más de un conjunto de sentencias de un case, deben colocarse break al final de
cada bloque. Es decir, las etiquetas case son puntos de entrada de la ejecución, y no implican que al acabarse
el bloque case la ejecución salte al final del bloque switch.
El bucle while sirve para ejecutar código reiteradas veces mientras se cumpla una determinada condición.
La cantidad de veces que el programa repita el código dependerá de la condición impuesta. Esta condición es
9
similiar a la que se utiliza en el bloue if. Primero se evalúa la condición. Si el resultado es verdadero, se ejecuta
el bloque de código. Luego se vuelve a evaluar la condición, y en caso de dar verdadera se vuelve a ejecutar el
bloque. El bucle se corta cuando la condición da falsa.
while(expresión)
{
sentencia;
...
sentencia;
}
Un tipo especial de bucle de programa es un bucle infinito. Se forma si la condición sigue sin cambios dentro
del bucle. La ejecución es simple en este caso ya que el resultado entre llaves es siempre verdadero (1=verdadero),
lo que significa que el programa se queda siempre dentro del bucle:
while(1)
{ // En vez de "while(1)", se puede escribir "while(true)"
... // Expresiones encerradas entre llaves se ejecutarán
... // repetidamente (bucle infinito)
}
Es muy parecida a la estructura anterior. Pero tiene una pequeña diferencia: en un bucle while, la compro-
bación de la expresión se hace al principio de cada ciclo. En cambio, en el do-while, se hace al final.
do
{
sentencias;
...
}while (condicion);
La expresión condicion se ejecuta al final del bucle, lo que significa que el bloque de código dentro del do se
ejecuta como mı́nimo una vez sin reparar en la veracidad o no de la condición. Si el resultado es verdadero, el
procedimiento se repite.
Este esta estructura sirve en los casos que se necesita que un bloque de sentencias se repita una cantidad de
veces previamente conocida. Esta estructura repetitiva se escribe como sigue:
expresioninicial es una o más sentencias, separadas por comas, que se ejecutan una única vez al entrar al
lazo.
expresiondecondicion es una expresión lógica, que se comprueba al principio de cada iteración. Mientras
resulte verdadera se continúa ejecutando el bucle.
cambiarexpresion es una o más sentencias, separadas por comas, que se realizan al final de cada ejecución
del cuerpo de la iteración.
10
La ejecución de esta secuencia de programa es similar al bucle while, salvo que en este caso el proceso de
especificar el valor inicial (inicialización) se realiza en la declaración. La expresioninicial especifica la variable
inicial del bucle, que más tarde se compara con la expresiondecondicion antes de entrar al bucle. Las operaciones
dentro del bucle se ejecutan repetidamente y después de cada iteración el valor de la expresioninicial varı́a de
acuerdo con la regla cambiarexpresion. La iteración continúa hasta que la expresiondecondicion es falsa.
En el ejemplo anterior, la operación se ejecutará cinco veces. Luego, al comprobarse que la expresión k¡5 es
falsa (después de 5 iteraciones k=5), el programa saldrá del bucle for.
A veces es necesario detener la ejecución de una estructura de control, salir de la misma y continuar con la
ejecución del programa. La sentencia break se puede utilizar dentro de cualquier estructura repetitiva (while, for,
do-while) y en la estructura switch también. En éstas la sentencia break se utiliza para salir de la estructura switch
si la condición case es verdadera. Por otro lado, en las estructuras repetitivas causa la inmediata culminación
del bucle, incluso si la condición de salida no se ha cumplido.
5.3.2. Continue
Se utiliza dentro de las estructuras repetitivas para saltar una iteración. A diferencia de la sentencia break,
el programa se queda dentro del bucle y las iteraciones continúan.
5.3.3. Go to
Permite hacer un salto absoluto a otro punto en el programa (“Go to” o “ir a”). Esta caracterı́stica se debe
utilizar con precaución dentro de C ya que su ejecución causa un salto incondicional sin hacer caso a todos
los tipos de limitaciones de anidación. El punto destino es identificado por una etiqueta, utilizada como un
argumento para la sentencia goto. Una etiqueta consiste en un identificador válido seguido por dos puntos(:).
...
if(CO2_sensor==1) goto aire acondicionado; // Si se consta que el valor
... // de la variable CO2_sensor =1
// hacer salto a la lı́nea de
// programa Aire acondicionado
...
Aire acondicionado: // Desde aquı́ sigue la parte del código
// que se ejecutará
// en caso de una concentración de
... // CO2 demasiado alta en el ambiente
Esta sentencia se muestra solo a modo ilustrativo. No está permitido su uso dentro de este curso dado
que va en contra de las buenas prácticas de programación. Su implementación no permite hacer una lectura y
seguimiento fluı́do del código. Adicionalmente existen estructuras con las que pueden resolverse los casos que se
resolverı́an con go to.
11
5.3.4. Return
Se utiliza para devolver el control al programa principal luego de ejecutar una porción de código perteneciente
a una función. Se volverá sobre esta sentencia más adelante durante este mismo apunte.
6. Funciones
Una función es una porción de código que se programa para desarrollar una tarea especı́fica. El objetivo
principal de la utilización de funciones dentro de C es el de hacer un código más ordenado y legible, a la vez que
se simplifica el problema a resolver dividiéndolo en varios procedimientos más sencillos. Las funciones dentro
de C son, conceptualmente, similares a las funciones vistas en el análisis matemático. Para ejemplificar esto, se
considera el siguiente ejemplo:
F (x) = x2 + 3x − 4
return expresion;
Como se deduce a partir del ejemplo anterior y tal como se mencinó al inicio de esta sección, las funciones
pueden tener argumentos de entrada, los cuales van entre parentesis a la derecha del nombre de la función.
Además, pueden tener argumentos de salida, los cuales se especifican con la expresión return. El tipo de dato
del argumento de salida se especifica a la izquierda del nombre de la función.
En el siguiente ejemplo se muestra el código de una función para calgular el área de un rectángulo, a partir de
conocer los datos de su base y altura:
return area;
}
Debe tenerse en cuenta que el tipo de dato del argumento de salida debe ser el mismo que el tipo de dato
de la variable que se quiere retornar.
12
6.1. Prototipos de funciones
Tal como ocurre con las variables, el uso de una función debe estar precedido por su declaración. Un ejemplo
de esto se muestra en el siguiente código:
if(x >= y)
{
valor = x;
}
else
{
valor = y;
}
return valor;
}
void main(void)
{
c = maximo(a, b); //se llama la función maximo dentro del mail().
printf("El maximo is %d\n", c)
}
Otra posibilidad es que en vez de declarar la función completa antes de usarla, se declare solo su prototipo.
Esto hace que el compilador “sepa” que la función existe y que debe buscar su declaración en los archivos del
proyecto. Se reescribe el código del ejemplo anterior, pero con la utilización de prototipo.
void main(void)
{
c = maximo(a, b); //se llama la función maximo dentro del mail().
printf("El maximo is %d\n", c)
}
int maximo(int x, int y) //se declara la función maximo, luego de haber sido usada.
{
unsigned int valor;
if(x >= y)
{
valor = x;
}
else
{
valor = y;
13
}
return valor;
}
14
Referencias
[1] Eduardo Grosclaude, Lenguaje C. Universidad Nacional del Comahue (2001).
[2] Carlos Canal, Gustavo Monte, Programación en C sobre PIC24. Capı́tulo de C. SASE 2014.
15