Catto / Mapa de Temas · Informática Electrónica I 4° año
Informática Electrónica I · 72 h · Eje 1 de 3

Elementos básicos del lenguaje C

C es el idioma de la electrónica. Con él se programan los microcontroladores de 6° y 7° año, los sistemas embebidos de la industria y el núcleo de casi todos los sistemas operativos. Es exigente, pero da control total sobre la máquina.

Programación Tipos de datos Control de flujo Funciones Preprocesador

01Por qué C en una tecnicatura en electrónica

Existen lenguajes más cómodos, pero ninguno reúne estas cuatro cosas a la vez:

  • Acceso directo al hardware. Permite escribir un valor en un registro del microcontrolador o leer un puerto de entrada tal como está en la hoja de datos.
  • Eficiencia. El código generado es casi tan compacto y rápido como el escrito en assembler, y eso importa cuando hay 2 KB de memoria.
  • Portabilidad. El mismo programa, con ajustes mínimos, compila para un PIC, un AVR, un ARM o una PC.
  • Universalidad. Todo fabricante de microcontroladores entrega su compilador de C. Es el denominador común de la industria.
C es un lenguaje compilado

El programa fuente (.c) pasa por el preprocesador, después por el compilador que lo traduce a código objeto (.o), y finalmente por el enlazador (linker), que une los objetos y las bibliotecas y produce el ejecutable. En un microcontrolador ese resultado es un archivo .hex que se graba en la memoria flash. Entender esta cadena es lo que permite interpretar los mensajes de error.

02Estructura de un programa

#include <stdio.h>      // directivas al preprocesador
#define PI 3.14159    // constante simbólica

float area(float radio);   // prototipo de función

int main(void)             // TODO programa tiene un main
{
    float r = 2.5;         // declaración con inicialización
    printf("Area = %.2f\n", area(r));
    return 0;                // 0 = terminó sin error
}

float area(float radio)    // definición de la función
{
    return PI * radio * radio;
}
ElementoPara qué está
#includeIncorpora un archivo de cabecera con declaraciones ya hechas. Con <> busca en las bibliotecas del sistema; con "", en el directorio del proyecto.
#defineDefine una constante o macro que el preprocesador reemplaza textualmente antes de compilar.
PrototipoLe anuncia al compilador cómo se llama una función, qué recibe y qué devuelve, antes de que aparezca su código.
main()Punto de entrada. En un microcontrolador nunca termina: su cuerpo suele ser un while(1).
{ }Delimitan bloques. Todo lo declarado dentro de un bloque sólo existe ahí.
;Termina cada sentencia. Olvidarlo es el error de compilación número uno.
C distingue mayúsculas de minúsculas

contador, Contador y CONTADOR son tres variables distintas. Por convención se usan minúsculas para variables y funciones, y MAYÚSCULAS para constantes definidas con #define.

03Variables, tipos y constantes

Toda variable debe declararse antes de usarse, indicando su tipo. Ese tipo determina cuánta memoria ocupa y qué valores puede guardar.

TipoTamaño típico
(microcontrolador de 8 bits)
RangoUso
char1 byte−128 a 127Un carácter, o un entero chico con signo.
unsigned char1 byte0 a 255El tipo más usado en microcontroladores: un puerto, un byte.
int2 bytes−32.768 a 32.767Enteros de propósito general. Ojo: en una PC son 4 bytes.
unsigned int2 bytes0 a 65.535Contadores, valores de un conversor A/D de 10 bits.
long4 bytes± 2.147.483.648Milisegundos transcurridos, frecuencias.
float4 bytes± 3,4 × 1038, 7 dígitosCálculos con decimales. Caro en un micro sin unidad de punto flotante.
double8 bytes15 dígitosPrecisión doble. Rara vez justificable en embebidos.
void«Nada». Función que no devuelve valor o que no recibe parámetros.
El tamaño de int depende de la máquina

El estándar de C sólo garantiza mínimos, no tamaños exactos. Por eso en programación embebida se usan los tipos de <stdint.h>, que son inequívocos: uint8_t, int16_t, uint32_t. El número es la cantidad de bits y la u indica sin signo. Es la práctica profesional recomendada y evita errores al portar código.

Desbordamiento

unsigned char c = 250;
c = c + 10;     // NO da 260: da 4, porque 260 no entra en 8 bits

unsigned char i;
for (i = 0; i < 300; i++) { ... }   // BUCLE INFINITO: i nunca llega a 300

El desbordamiento no genera ningún error: el valor simplemente «da la vuelta». Es una de las causas más difíciles de encontrar en un programa embebido, y por eso hay que elegir el tipo pensando en el máximo valor posible, no en el habitual.

Constantes

#define LARGO 10            // preprocesador: reemplazo textual, sin tipo
const float PI = 3.14159;   // variable de sólo lectura, CON tipo

int a = 255;      // decimal
int b = 0xFF;     // hexadecimal — el más usado con registros
int c = 0b11111111; // binario (extensión de GCC, muy cómoda para puertos)
int d = 0377;     // OCTAL: el 0 inicial cambia la base. Trampa clásica

En electrónica el hexadecimal es el formato natural: cada dígito hex representa exactamente 4 bits, así que 0xF0 se lee de un vistazo como «los cuatro bits altos en 1».

04Operadores

Aritméticos y de asignación

+ − * / % · ++ -- · = += -= *= /=

El % es el resto de la división entera. / entre enteros trunca: 7/2 da 3, no 3,5.

Relacionales y lógicos

== != < > <= >= · && || !

En C, 0 es falso y cualquier otro valor es verdadero.

Operadores a nivel de bit: los imprescindibles

Son los que hacen a C apto para electrónica. Permiten manipular bits individuales de un registro sin tocar los demás.

OperadorFunciónUso típico
&AND bit a bitPoner bits en 0 (enmascarar) y consultar un bit.
|OR bit a bitPoner bits en 1.
^XOR bit a bitInvertir bits (toggle).
~ComplementoInvertir todos los bits.
<<Desplazar a izquierdaConstruir máscaras. Equivale a multiplicar por 2.
>>Desplazar a derechaExtraer campos. Equivale a dividir por 2.
// Las cuatro operaciones que se usan todo el tiempo con registros:

PORTB |=  (1 << 3);   // poner en 1 el bit 3, sin tocar los otros
PORTB &= ~(1 << 3);   // poner en 0 el bit 3
PORTB ^=  (1 << 3);   // invertir el bit 3

if (PINB & (1 << 3)) { // ¿el bit 3 está en 1?
    // sí: el pulsador está apretado
}
Por qué no se escribe PORTB = 8;

Porque eso pondría el bit 3 en 1 y todos los demás en 0, apagando el resto de las salidas del puerto. Las formas con |= y &= ~ modifican sólo el bit que interesa. Es la diferencia entre un programa que funciona y uno que apaga el motor cada vez que prende el LED.

El error más frecuente de todos: = contra ==
if (x = 5)   // ASIGNA 5 a x, y como 5 es distinto de 0, SIEMPRE es verdadero
if (x == 5)  // COMPARA x con 5. Esto es lo que se quería

No es un error de compilación: el programa compila y anda mal. Los compiladores modernos avisan con un warning, y por eso hay que compilar siempre con -Wall y leer las advertencias.

05Control de flujo

Decisión
if (temp > 30) {
    encender_ventilador();
} else if (temp < 18) {
    encender_estufa();
} else {
    apagar_todo();
}

switch (tecla) {
    case 1: subir();  break;
    case 2: bajar();  break;
    default: parar();
}
Repetición
for (i = 0; i < 8; i++) {
    encender_led(i);
}

while (!dato_listo()) {
    // espera
}

do {
    v = leer_adc();
} while (v < umbral);

while (1) { ... }  // bucle principal
Olvidar el break en un switch

Sin break, la ejecución continúa cayendo en el caso siguiente. A veces se hace a propósito (para agrupar casos), pero casi siempre es un olvido y produce un comportamiento desconcertante.

El bucle principal de un sistema embebido
int main(void)
{
    configurar_puertos();   // se ejecuta UNA vez
    configurar_adc();
    configurar_timers();

    while (1) {              // se repite PARA SIEMPRE
        leer_sensores();
        procesar();
        actualizar_salidas();
    }
    return 0;   // nunca se llega acá
}

Esta estructura —configuración + bucle infinito— es la de todo programa de microcontrolador, y es literalmente lo que hacen las funciones setup() y loop() de Arduino, que es C por debajo.

06Arreglos y cadenas

Un arreglo es un conjunto de elementos del mismo tipo, contiguos en memoria, accesibles por un índice que empieza en 0.

int muestras[10];              // índices válidos: 0 a 9
int tabla[5] = {2, 4, 6, 8, 10};

muestras[0] = 512;             // primer elemento
muestras[9] = 128;             // ÚLTIMO elemento
muestras[10] = 0;              // ¡FUERA DE RANGO! Pisa memoria ajena

// Promediar 10 muestras del conversor A/D
long suma = 0;
for (int i = 0; i < 10; i++) {
    muestras[i] = leer_adc();
    suma += muestras[i];
}
int promedio = suma / 10;
C no controla los límites de un arreglo

Escribir muestras[10] en un arreglo de 10 elementos no da error: escribe en la posición de memoria siguiente, que puede pertenecer a otra variable, a la pila o a un registro. El programa sigue andando y falla más adelante de forma inexplicable. En una PC esto es un agujero de seguridad clásico (buffer overflow); en un microcontrolador, un cuelgue aleatorio. Verificar los índices es responsabilidad del programador.

Cadenas de caracteres

En C una cadena es simplemente un arreglo de char terminado por el carácter nulo '\0'. No es un tipo especial.

char nombre[20] = "Electronica";   // ocupa 11 letras + 1 del '\0' = 12

#include <string.h>
strlen(nombre);            // 11 — no cuenta el '\0'
strcpy(destino, origen);   // copiar
strcmp(a, b);              // comparar: devuelve 0 si son iguales
strcat(destino, origen);   // concatenar
Hay que reservar lugar para el '\0'

Un arreglo de 20 char guarda como máximo una cadena de 19 caracteres. Y strcmp(a, b) devuelve 0 cuando son iguales, lo que es contraintuitivo: se escribe if (strcmp(a,b) == 0). Nunca comparar cadenas con ==, porque eso compara direcciones de memoria, no contenidos.

07Funciones

Dividir el programa en funciones no es cosmética: es lo que permite probar cada parte por separado, reutilizar código y entender un programa de mil líneas.

// tipo_devuelto  nombre(tipo parám1, tipo parám2)
float celsius_a_fahrenheit(float c)
{
    return c * 9.0 / 5.0 + 32.0;
}

void parpadear(unsigned char veces)   // void: no devuelve nada
{
    for (unsigned char i = 0; i < veces; i++) {
        PORTB |= (1 << 0);
        retardo_ms(200);
        PORTB &= ~(1 << 0);
        retardo_ms(200);
    }
}
En C los parámetros se pasan por valor

La función recibe una copia del argumento. Si la modifica, el original no cambia. Para que una función pueda modificar una variable del que la llamó hay que pasarle su dirección, con punteros — que es el tema central del eje siguiente.

Ámbito de las variables

TipoDónde viveCuándo usarla
LocalDeclarada dentro de una función. Nace al entrar y muere al salir.Siempre que se pueda. Es lo más seguro y ahorra memoria.
GlobalFuera de toda función. Existe durante todo el programa.Sólo para datos que comparten varias partes, típicamente entre el bucle principal y una interrupción.
Estáticastatic dentro de una función: conserva su valor entre llamadas.Contadores internos que no deben ser visibles desde afuera.
Volátilvolatile: le avisa al compilador que puede cambiar por fuera del flujo del programa.Obligatorio en variables modificadas por una interrupción o mapeadas a un registro de hardware.
Sin volatile, el optimizador rompe el programa

Si una variable global se modifica dentro de una interrupción y el bucle principal la lee, el compilador puede «darse cuenta» de que nadie la cambia dentro del bucle y guardarla en un registro para siempre. El programa queda esperando eternamente un cambio que no ve. La palabra volatile le prohíbe esa optimización. Es un error que aparece recién al compilar con optimización activada, y es un clásico de 6° año.

08El preprocesador y la biblioteca estándar

El preprocesador actúa antes del compilador y hace reemplazos de texto. No entiende de tipos ni de sintaxis de C.

#include <stdio.h>       // pegar el contenido de ese archivo acá
#define LED_ON  PORTB |= 1
#define MAX(a,b) ((a) > (b) ? (a) : (b))   // macro con parámetros

#ifdef DEBUG              // compilación condicional
    printf("valor = %d\n", x);
#endif

#ifndef CONFIG_H          // guarda de inclusión: evita incluir
#define CONFIG_H          // dos veces el mismo archivo
    ...
#endif
Los paréntesis en las macros no son opcionales

Con #define DOBLE(x) x*2, la expresión DOBLE(3+1) se expande a 3+1*2 = 5, no 8. Por eso toda macro se escribe envolviendo cada parámetro y el conjunto entre paréntesis: #define DOBLE(x) ((x)*2). Cuando se puede, es preferible una función inline, que sí respeta los tipos.

Las bibliotecas que se usan

CabeceraContieneNota para embebidos
<stdio.h>printf, scanf, archivosprintf ocupa varios KB: en un micro chico se reemplaza por una función propia de envío por serie.
<stdlib.h>atoi, rand, mallocEvitar malloc en embebidos: fragmenta la memoria y puede fallar sin aviso.
<string.h>Manejo de cadenas y de memoriamemset y memcpy son muy útiles con buffers.
<math.h>sin, sqrt, powMuy pesada. Si alcanza, usar aritmética entera o tablas precalculadas.
<stdint.h>uint8_t, int16_t, …Usar siempre en programación de microcontroladores.
<stdbool.h>bool, true, falseMejora la legibilidad sin costo.

09En el laboratorio

Práctica 1 · Primer programa y ciclo de compilación

Con GCC en línea de comandos (no con un entorno gráfico, para ver los pasos):

gcc -Wall -c hola.c -o hola.o    # compilar a objeto
gcc hola.o -o hola               # enlazar
./hola                           # ejecutar
gcc -E hola.c > hola.i           # ver la salida del PREPROCESADOR

Abrir hola.i y comprobar que el #include se convirtió en cientos de líneas y que los #define desaparecieron reemplazados por su valor. Es la forma más clara de entender qué hace cada etapa.

Práctica 2 · Tipos y desbordamiento

Escribir un programa que declare un unsigned char en 250 y le sume 1 en un bucle, imprimiendo el valor. Observar cómo salta de 255 a 0. Después imprimir sizeof(int), sizeof(long), sizeof(float) y comparar con lo que dicen los apuntes: en la PC del taller int va a ocupar 4 bytes, no 2.

Práctica 3 · Manipulación de bits

Simular un puerto con una variable unsigned char puerto = 0; y escribir funciones set_bit(n), clear_bit(n), toggle_bit(n) y get_bit(n). Imprimir el valor en binario después de cada operación. Es exactamente lo que se va a hacer con registros reales en Electrónica Digital III.

Práctica 4 · Conversión de un valor de conversor A/D

Escribir una función que reciba el valor crudo de un conversor A/D de 10 bits (0 a 1023) y lo convierta a tensión con referencia de 5 V (conviene tener a mano el conversor de prefijos para no perderse entre milivolt y volt). Hacerlo de dos formas: con float y con aritmética entera en milivolt. Comparar los resultados y reflexionar sobre cuál conviene en un microcontrolador de 8 bits. Es el cálculo que va a aparecer en todos los proyectos de sensores.

10Errores frecuentes

ErrorConsecuencia
Falta un ;Error de compilación, señalado a veces en la línea siguiente. Siempre revisar la línea de arriba de la que marca el compilador.
= en vez de == en un ifCompila, y la condición siempre resulta verdadera. Compilar con -Wall.
Índice de arreglo fuera de rangoNo hay error: se pisa memoria ajena y el fallo aparece mucho después, en otro lado.
Variable usada sin inicializarContiene basura. En una PC suele dar cero por casualidad; en un micro, cualquier cosa.
División entera cuando se esperaban decimales7/2 da 3. Hay que escribir 7.0/2 o hacer un cast.
Falta volatile en una variable de interrupciónEl optimizador la «congela» y el programa se cuelga esperando un cambio que no ve.
Falta break en un switchSe ejecutan también los casos siguientes.
Comparar cadenas con ==Compara direcciones de memoria, no contenidos. Hay que usar strcmp.
Macro sin paréntesisResultados aritméticos inesperados por precedencia de operadores.

11Autoevaluación

¿Cuáles son las tres etapas por las que pasa un programa en C hasta ser ejecutable?

Preprocesador (resuelve #include y #define), compilador (traduce a código objeto) y enlazador (une los objetos con las bibliotecas y genera el ejecutable o el .hex).

¿Qué imprime unsigned char c = 200; c = c + 100;?

44. 300 no entra en 8 bits: 300 − 256 = 44. Es un desbordamiento silencioso.

Escribir la instrucción que pone en 1 el bit 5 de PORTC sin alterar los demás.

PORTC |= (1 << 5);
Para ponerlo en 0: PORTC &= ~(1 << 5);
Para invertirlo: PORTC ^= (1 << 5);

¿Cuántos caracteres útiles entran en char texto[16];?

15. El decimosexto lugar lo ocupa el terminador '\0'.

¿Por qué if (strcmp(a, b)) hace lo contrario de lo que parece?

Porque strcmp devuelve 0 cuando las cadenas son iguales, y en C el 0 es falso. Es decir, esa condición se cumple cuando son distintas. Hay que escribir if (strcmp(a, b) == 0).

¿Qué diferencia hay entre una variable local y una estática dentro de una función?

La local se crea al entrar a la función y se destruye al salir, perdiendo su valor. La estática (static) conserva su valor entre llamadas sucesivas, aunque sigue siendo invisible desde afuera de la función.

¿Cuándo es obligatorio declarar una variable como volatile?

Cuando puede cambiar por fuera del flujo normal del programa: variables modificadas por una interrupción, o mapeadas a un registro de hardware. Sin volatile, el optimizador puede asumir que nadie la cambia y usar una copia en un registro.

¿Por qué se recomienda usar uint8_t en vez de unsigned char?

Porque el tamaño de los tipos básicos de C depende de la plataforma. Los tipos de <stdint.h> declaran explícitamente la cantidad de bits, así que el programa se comporta igual al portarlo a otro microcontrolador o a una PC.

¿Qué imprime printf("%d", 7/2);? ¿Y cómo se obtiene 3,5?

Imprime 3: la división entre dos enteros es entera y trunca. Para obtener 3,5 hay que forzar punto flotante: 7.0/2 o (float)7/2, e imprimirlo con %f.

Desarrollo del eje «Elementos básicos del lenguaje C» de Informática Electrónica I (4° año), según la «Propuesta Curricular – Segundo Ciclo de la Modalidad Técnico Profesional, Educación Secundaria – Electrónica», Ministerio de Educación de la Provincia de Córdoba, DGETyFP. Volver al Mapa de Temas · catto.ar