Tema 3

Programación de sistemas embebidos en lenguaje C

Contenidos

  • Introducción.

  • Tipos de datos y constantes.

  • Variables.

  • Operadores y expresiones.

  • Control de flujo.

  • Preprocesador y macros.

  • Funciones y bibliotecas.

  • Uso de memoria y punteros.

  • Estructuras de datos.

Introducción

Historia del lenguaje C

  • Creado por Dennis Ritchie a partir de 1972.

  • Publicación de The C Programming Language en 1978.

  • Publicación del estándar ANSI C en 1983.

  • Publicación de The C Programming Language (ANSI C) en 1988.

  • Revisiones del estándar ANSI C (C89, C90, C11, C17, C2x…​)

  • Actualmente, TIOBE index.

Compilador de C

Ejemplo de un programa en C

  • Online C compiler:

    /* Example program. */
    #include <stdio.h>
    
    int main(void)
    {
        printf("Hello world\n");
    
        return 0;
    }
  • Un programa contiene comentarios, funciones y variables.

  • Una función contiene comentarios, variables y sentencias. → Definición

  • Una función acepta una lista de argumentos y devuelve un resultado. → Declaración

  • La declaración de funciones suele realizarse mediante la inclusión de archivos de cabecera (.h).

Biblioteca estándar de entrada/salida

  • Función printf:

    int printf(const char *format, ...)
    • El formato es una cadena de texto en la que se incluyen especificadores de conversión precedidos de %.

    • Algunos especificadores de conversión comunes:

      d    Entero con signo
      u    Entero sin signo
      x    Entero hexadecimal
      
      f    Real coma flotante
      
      c    Carácter de texto
      s    Cadena de texto

Palabras reservadas

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

Tipos de datos y constantes

Tipos de datos

signed char               Carácter con signo (8 bits).
unsigned char             Carácter sin signo (8 bits, byte).

signed short int          Entero corto con signo (16 bits).
unsigned short int        Entero corto sin signo (16 bits, word).

signed int                Entero con signo (16 bits*).
unsigned int              Entero sin signo (16 bits*).

signed long int           Entero largo con signo (32 bits*).
unsigned long int         Entero largo sin signo (32 bits*, dword).

signed long long int      Entero extra largo con signo (64 bits).
unsigned long long int    Entero extra largo sin signo (64 bits, qword).

float                     Real coma flotante (32 bits, precisión simple).
double                    Real coma flotante (64 bits, precisión doble).
long double               Real coma flotante (80 bits*, precisión extendida).

void                      Vacío.

(*) Depende de la arquitectura del procesador

Solución alternativa <stdint.h>

Moldeado de datos (casting)

  • Una expresión precedida por un tipo de datos entre paréntesis, causa la conversión del valor de la expresión al tipo solicitado.

  • Sintaxis:

    (tipo_dato)expresión;
  • Ejemplo:

    int x, y;
    double d;
    
    d = sqrt((double)x*x + (double)y*y);

Constantes

  • Expresiones que se evalúan a un valor constante.

  • Tipos de constantes utilizadas habitualmente:

    • Carácter de texto como 'A'.

    • Carácter de texto especial como '\n', '\r', '\t'.

    • Cadena de texto como "Hola".

    • Valor entero como 1234

    • Valor real como 123.4 o 1234e-1

    • Valor entero en hexadecimal como 0x2A0F

Constantes

  • Constantes simbólicas:

    #define PI          3.1416
  • Constantes enumeradas

    enum months {
        JAN = 1, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC
    };
    • Los valores son enteros con signo.

    • Si no se especifica explícitamente, al primer elemento se le asigna el valor 0.

Variables

Variables

  • Contenedores para almacenar valores en memoria.

  • Declaración de variables:

    modificador tipo_dato nombre_variable = valor_inicial;
  • Reglas aplicadas al nombre de las variables:

    • Sólo puede contener los caracteres a-z, A-Z, 0-9 y _.

    • No puede empezar por un 0-9.

    • Se distingue entre mayúsculas y minúsculas.

    • No se pueden utilizar palabras reservadas.

  • Ámbito (global o local).

  • Modificadores (extern, register, static, volatile, const).

Ejemplo 1

  • Tabla de conversión de ºC a ºF (versión en coma fija)

    #include <stdio.h>
    
    int main(void)
    {
        int fahr, celsius;
    
        fahr = 0;
        while (fahr <= 300) {
            celsius = (5 * (fahr - 32)) / 9;
            printf("%d\t%d\n", fahr, celsius);
            fahr = fahr + 20;
        }
    
        return 0;
    }

Ejemplo 2

  • Tabla de conversión de ºC a ºF (versión en coma flotante)

    #include <stdio.h>
    
    int main(void)
    {
        float fahr, celsius;
    
        fahr = 0;
        while (fahr <= 300) {
            celsius = (5 * (fahr - 32)) / 9;
            printf("%f\t%f\n", fahr, celsius);
            fahr = fahr + 20;
        }
    
        return 0;
    }

Operadores y expresiones

Operadores

Aritméticos

+ - * / %

Lógicos

&& || !

Relacionales

== != > >= < <=

Bits

& | ^ << >> ~

Autoincremento y autodecremento

++ --

Ternario

?:

Expresiones

  • Prioridad de los operadores en la evaluación de expresiones:

    1. * / %

    2. + -

    3. << >>

    4. > >= < <=

    5. == !=

    6. &

    7. ^

    8. |

    9. &&

    10. ||

    11. = += -= *= /= %= &= ^= |= <<= >>=

Control de flujo

Sentencias y bloques

  • Las sentencias son expresiones terminadas en ;.

  • Los bloques agrupan varias sentencias entre { }.

  • Un bloque se comporta a efectos prácticos como una sentencia.

  • Se pueden declarar variables locales dentro de un bloque.

If-Else

  • Estructura de control para toma de decisiones simples.

  • Sintaxis:

    • Con una sentencia:

      if (expresión)
          sentencia;
      else
          sentencia;
    • Con un bloque:

      if (expresión) {
          sentencias;
      } else {
          sentencias;
      }

Ejemplo 3

  • Determinar a qué semestre del año pertenece un mes

    #include <stdio.h>
    
    enum months {
        JAN = 1, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC
    };
    
    int main(void)
    {
        enum months month;
    
        month = DEC;
    
        if (month >= JUL) {
            printf("Segundo semestre\n");
        } else {
            printf("Primer semestre\n");
        }
    
        return 0;
    }

Else-If

  • Estructura de control para toma de decisiones múltiples.

  • Sintaxis:

    • Con una sentencia:

      if (expresión)
          sentencia;
      else if (expresión)
          sentencia;
      else if (expresión)
          sentencia;
      else
          sentencia;
    • Con un bloque:

      if (expresión) {
          sentencias;
      } else if (expresión) {
          sentencias;
      } else if (expresión) {
          sentencias;
      } else {
          sentencias;
      }

Ejemplo 4

  • Determinar a qué trimestre del año pertenece un mes

    #include <stdio.h>
    
    enum months {
        JAN = 1, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC
    };
    
    int main(void)
    {
        enum months month;
    
        month = DEC;
    
        if (month >= OCT) {
            printf("Cuarto trimestre\n");
        } else if (month >= JUL) {
            printf("Tercer trimestre\n");
        } else if (month >= APR) {
            printf("Segundo trimestre\n");
        } else {
            printf("Primer trimestre\n");
        }
    
        return 0;
    }

Switch

  • Estructura de control para selección de casos múltiples.

  • Sintaxis:

    switch (expresión) {
        case constante1:
            sentencias;
            break;
        case constante2:
            sentencias;
            break;
        default:
            sentencias;
            break;
    }
  • Los valores de los casos son constantes enteras con signo.

Ejemplo 5

  • Mostrar el mes del año como una cadena de texto

    #include <stdio.h>
    
    enum months {
        JAN = 1, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC
    };
    
    int main(void)
    {
        enum months month;
    
        month = DEC;
    
        switch (month) {
            case JAN: printf("Enero\n");      break;
            case FEB: printf("Febrero\n");    break;
            case MAR: printf("Marzo\n");      break;
            case APR: printf("Abril\n");      break;
            case MAY: printf("Mayo\n");       break;
            case JUN: printf("Junio\n");      break;
            case JUL: printf("Julio\n");      break;
            case AUG: printf("Agosto\n");     break;
            case SEP: printf("Septiembre\n"); break;
            case OCT: printf("Octubre\n");    break;
            case NOV: printf("Noviembre\n");  break;
            case DEC: printf("Diciembre\n");  break;
        }
    
        return 0;
    }

For

  • Estructura de control para repetir una sentencia un número de veces determinado por tres expresiones: inicio, condición e incremento.

  • Sintaxis:

    • Con una sentencia:

      for (expresión_inicio; expresión_condición; expresión_incremento)
          sentencia;
    • Con un bloque:

      for (expresión_inicio; expresión_condición; expresión_incremento) {
          sentencias;
      }

Ejemplo 6

  • Tabla de conversión de ºC a ºF (versión en coma flotante)

    #include <stdio.h>
    
    int main(void)
    {
        float fahr, celsius;
    
        for (fahr = 0; fahr <= 300; fahr += 20) {
            celsius = (5 * (fahr - 32)) / 9;
            printf("%f\t%f\n", fahr, celsius);
        }
    
        return 0;
    }

Ejemplo 7

  • Cuidado con las sentencias y los bloques

    #include <stdio.h>
    
    int main(void)
    {
        int n, i;
    
        n = 10;
    
        if (n > 0)
            for (i = 0; i < n; i++)
                if ((i & 1) != 0)
                    printf("%d\n", i);
        else
            printf("n es negativo.\n");
    
        return 0;
    }

While

  • Estructura de control para repetir una sentencia mientras se cumpla una expresión evaluada antes de la primera iteración del bucle.

  • Sintaxis:

    • Con una sentencia:

      while (expresión)
          sentencia;
    • Con un bloque:

      while (expresión) {
          sentencias;
      }

While

  • Un bucle For:

    for (expresión_inicio; expresión_condición; expresión_incremento) {
        sentencias;
    }
  • Equivale al siguiente bucle While que utiliza las mismas tres expresiones del bucle For:

    expresión_inicio;
    while (expresión_condición) {
        sentencias;
        expresión_incremento;
    }

Do-While

  • Estructura de control para repetir una sentencia mientras se cumpla una expresión evaluada tras la primera iteración del bucle.

  • Sintaxis:

    • Con una sentencia:

      do
          sentencia;
      while (expresión);
    • Con un bloque:

      do {
          sentencias;
      } while (expresión);

Break/Continue

  • La sentencia Break permite salir de un bucle For, While o Do-While igual que de un Switch.

  • Sintaxis:

    while (expresión_while) {
        sentencias;
        if (expresión_break)
            break;
        sentencias;
    }

Break/Continue

  • La sentencia Continue salta a la siguiente iteración de un bucle For, While o Do-While.

  • Sintaxis:

    while (expresión_while) {
        sentencias;
        if (expresión_continue)
            continue;
        sentencias;
    }

Ejemplo 8

  • Ejemplo de utilización de Break/Continue

    #include <stdio.h>
    
    int main(void)
    {
        int i;
    
        for (i = 0; i <= 10; i++) {
            if (i == 5) {
                break;
            }
            printf("%d ", i);
        }
        printf("\n");
    
        for (i = 0; i <= 10; i++) {
            if (i == 5) {
                continue;
            }
            printf("%d ", i);
        }
        printf("\n");
    
        return 0;
    }

Goto-Label

  • Salto incondicional a una etiqueta.

  • Sintaxis:

        sentencias;
        goto etiqueta;
        sentencias;
    etiqueta:
        sentencias;
  • No se recomienda su uso salvo en algunos casos muy concretos.

Goto-Label

  • Ejemplo de utilización de Goto/Label:

        for (i = 0; i < n; i++)
            for (j = 0; j < m; j++)
                if (a[i] == b[j])
                    goto found;
    
        /* not found */
        ...
        goto done;
    
    found:
        /* found */
        ...
    
    done:
  • Alternativa, código equivalente sin utilizar Goto-Label:

        found = 0;
        for (i = 0; (i < n) && !found; i++)
            for (j = 0; (j < m) && !found; j++)
                if (a[i] == b[j])
                    found = 1;
    
        if (found) {
            /* found */
            ...
        } else {
            /* not found */
            ...
        }

Preprocesador y macros

Preprocesador

  • Conceptualmente es un paso independiente previo a la compilación.

  • El preprocesador evalua las líneas que empiezan por #.

  • Los usos más frecuentes del preprocesador son:

    • Inclusión de archivos #include.

    • Macros de sustitución #define.

    • Compilación condicional #ifdef, #if.

Inclusión de archivos

  • Generalmente se utiliza #include al principio del programa para incluir los archivos de cabecera (.h) con la declaración de las funciones de las bibliotecas utilizadas.

  • Sintaxis:

    #include <nombre.h>
    #include "nombre.h"

Macros de sustitución

  • Generalmente se utiliza #define para definir macros de sustitución (constantes simbólicas).

  • Sintaxis:

    #define nombre      reemplazo
  • También es posible definir macros de sustitución que acepten argumentos:

  • Ejemplo:

    #define max(A, B)   ((A) > (B) ? (A) : (B))
    #define min(A, B)   ((A) < (B) ? (A) : (B))
  • Se puede eliminar la definición de un macro de sustitución con #undef.

Compilación condicional

  • La compilación condicional se utiliza para incluir código de forma selectiva en función de condiciones evaluadas en el momento de la compilación.

  • Sintaxis:

    #if expresión
    ...
    #else
    ...
    #endif
  • Ejemplo:

    #if (DEBUG > 0)
    #define DEBUG_PRINT(error)      printf("Error %d\n", error)
    #else
    #define DEBUG_PRINT(error)
    #endif

Funciones y bibliotecas

Funciones

  • Permiten dividir una tarea larga en varias tareas más pequeñas.

  • Permiten su reutilización. → Bibliotecas.

  • Aceptan argumentos de entrada y permiten devolver un resultado.

  • Para poder utilizar una función primero hay que declararla o definirla.

  • Sintaxis:

    tipo_dato nombre_función(argumentos)
    {
        declaraciones;
    
        sentencias;
    
        return resultado;
    }

Ejemplo 9

  • Tabla de conversión de ºC a ºF (versión en coma flotante)

    #include <stdio.h>
    
    float convert_celsius(float fahr)
    {
        return (5 * (fahr - 32)) / 9;
    }
    
    int main(void)
    {
        float fahr;
    
        for (fahr = 0; fahr <= 300; fahr += 20) {
            printf("%f\t%f\n", fahr, convert_celsius(fahr));
        }
    
        return 0;
    }

Bibliotecas

  • Habitualmente las funciones se declaran en archivos de cabecera (.h) y se definen en archivos de código (.c).

  • Las funciones más utilizadas se agrupan en bibliotecas para facilitar su reutilización.

  • Bibliotecas estándar de C.

Uso de memoria y punteros

Cadenas de texto

  • Cadenas de texto constantes como "Hola".

  • Uso de memoria de una cadena de texto (formato ASCIIZ o null-terminated):

    'H'

    'o'

    'l'

    'a'

    0

  • Declaración de cadenas de texto variables:

    char nombre_cadena[longitud_cadena + 1] = "texto";

Ejemplo 10

  • Ejemplo de utilización de cadenas de texto

    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        char text[10];
        int i;
    
        printf("%ld\n", sizeof(text));
    
        strcpy(text, "uno");
        printf("(%ld) %s\n", strlen(text), text);
    
        strcat(text, " dos");
        printf("(%ld) %s\n", strlen(text), text);
    
        i = 0;
        while (text[i] != 0) {
            printf("%c", text[i]);
            i++;
        }
        printf("\n");
    
        return 0;
    }

Matrices

  • Permiten crear conjuntos de datos homogéneos unidimensionales (vectores) o multidimensionales (matrices).

  • Sintaxis:

    tipo_dato nombre_vector[elementos] = {valores};
    tipo_dato nombre_matriz[filas][columnas] = {{valores_fila1}, {valores_fila2}...};
  • Los índices de los elementos de una matriz siempre empiezan en cero.

Ejemplo 11

  • Ejemplo de utilización de matrices (vector y matriz 2-D)

    #include <stdio.h>
    
    enum months {
        JAN = 1, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC
    };
    
    const int month_days[12 + 1] = {
        0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
    };
    
    int main(void)
    {
        enum months month;
        int i, j;
        int matrix[3][5] = { { 1,  2,  3,  4,  5},
                             { 6,  7,  8,  9, 10},
                             {11, 12, 13, 14, 15} };
    
        /* Vector */
        month = DEC;
        printf("%d días\n", month_days[month]);
    
        /* Array */
        for (i = 0; i < 3; i++) {
            for (j = 0; j < 5; j++) {
                printf("%2d ", matrix[i][j]);
            }
            printf("\n");
        }
    
        return 0;
    }

Punteros

Punteros como variables

  • Un puntero es una variable que contiene la dirección de memoria de otra variable.

    c pointers
  • El operador & devuelve la dirección de una variable.

  • El operador * devuelve el valor de la variable a la que apunta el puntero.

Ejemplo 12

  • Ejemplo de utilización de punteros

    #include <stdio.h>
    
    int main(void)
    {
        int *a, b;
    
        b = 25;
    
        a = &b;
        printf("%d %d\n", *a, b);
    
        *a += 5;
        printf("%d %d\n", *a, b);
    
        return 0;
    }

Ejemplo 13

  • Internamente, las matrices son realmente punteros

    #include <stdio.h>
    
    int main(void)
    {
        char text[] = "Hola";
        char *p;
        int i;
    
        /* Array */
        i = 0;
        while (text[i] != 0) {
            printf("%c", text[i]);
            i++;
        }
        printf("\n");
    
        /* Pointer as array */
        p = text;
        i = 0;
        while (p[i] != 0) {
            printf("%c", p[i]);
            i++;
        }
        printf("\n");
    
        /* Pointer as pointer */
        p = text;
        while (*p != 0) {
            printf("%c", *p++);
        }
        printf("\n");
    
        return 0;
    }

Punteros como argumentos de funciones

  • Cuando se llama a una función todos los argumentos se pasan por valor.

  • La función trabaja con una copia del valor de los argumentos independiente de la variable original.

  • Si se desea que la función modifique el valor de los argumentos, es necesario pasarlos por referencia (como punteros).

Ejemplo 14

  • Ejemplo de paso de argumentos a una función por valor y por referencia

    #include <stdio.h>
    
    void swap_val(int x, int y)
    {
        int temp;
    
        temp = x;
        x = y;
        y = temp;
    }
    
    void swap_ref(int *px, int *py)
    {
        int temp;
    
        temp = *px;
        *px = *py;
        *py = temp;
    }
    
    int main(void)
    {
        int a = 5, b = 9;
    
        swap_val(a, b);
        printf("%d %d\n", a, b);
    
        swap_ref(&a, &b);
        printf("%d %d\n", a, b);
    
        return 0;
    }

Declaraciones complejas con punteros

  • Afortunadamente no se utilizan mucho en la práctica.

  • Se leen de dentro hacia afuera (no de izquierda a derecha):

    int *daytab[13]
        daytab: array[13] of pointer to int
    int (*daytab)[13]
        daytab: pointer to array[13] of int
    
    int *f();
        f: function returning pointer to int
    int (*pf)();
        pf: pointer to function returning int
    
    char (*(*x())[])()
        x: function returning pointer to array[] of pointer to function returning char
    char (*(*x[3])())[5]
        x: array[3] of pointer to function returning pointer to array[5] of char

Uso de memoria dinámico

  • El uso de memoria estático reserva el espacio de memoria al compilar el programa.

  • El uso de memoria dinámico permite solicitar espacio de memoria de tamaño variable en tiempo de ejecución.

  • Biblioteca estándar <stdlib.h>.

    void *malloc(size_t size);
    void *calloc(size_t num, size_t size);
    void free(void *ptr );

Estructuras de datos

Estructuras de datos

  • Permiten agrupar varias variables de tipos distintos bajo un sólo nombre para facilitar su manejo.

  • Sintaxis:

    struct nombre_estructura {
        tipo_dato variable;
        ...
    };
    
    struct nombre_estructura nombre_variable = {valores};
  • Para acceder a las variables de una estructura se utiliza nombre_variable.variable.

  • En el caso de un puntero a una estructura podemos utilizar (*nombre_puntero).variable o, la notación alternativa, nombre_puntero->variable.

Ejemplo 15

  • Ejemplo de utilización de estructuras

    #include <stdio.h>
    
    struct point {
        int x;
        int y;
    };
    
    int main(void)
    {
        struct point pt = {100, 200};
        struct point *p;
    
        /* Structure */
        printf("(%d, %d)\n", pt.x, pt.y);
    
        pt.x += 10;
        pt.y += 20;
    
        printf("(%d, %d)\n", pt.x, pt.y);
    
        /* Pointer to structure */
        p = &pt;
        (*p).x += 10;
        p->y += 20;
    
        printf("(%d, %d)\n", (*p).x, p->y);
    
        return 0;
    }

Typedefs

  • Permite crear tipos de datos nuevos (pseudónimos de tipos de datos existentes).

  • Sintaxis:

    typedef tipo_dato nombre_nuevo;
  • Ejemplo:

    typedef struct {
        int x;
        int y;
    } point;
    
    point pt1, pt2;

Ejemplo 16

  • Ejemplo de utilización de estructuras en funciones

    #include <stdio.h>
    
    typedef struct {
        int x;
        int y;
    } point;
    
    point addpoints(point pt1, point pt2)
    {
       pt1.x += pt2.x;
       pt1.y += pt2.y;
       return pt1;
    }
    
    int main(void)
    {
        point pt1 = {100, 200};
        point pt2 = { 10,  20};
        point pt3 = {  0,   0};
    
        printf("(%d, %d) (%d, %d) (%d, %d)\n", pt1.x, pt1.y,
                                               pt2.x, pt2.y,
                                               pt3.x, pt3.y);
    
        pt3 = addpoints(pt1, pt2);
    
        printf("(%d, %d) (%d, %d) (%d, %d)\n", pt1.x, pt1.y,
                                               pt2.x, pt2.y,
                                               pt3.x, pt3.y);
    
        return 0;
    }

Ejemplo 17

  • Ejemplo de utilización de estructuras con matrices unidimensionales

    #include <stdio.h>
    
    typedef enum {
        JAN = 1, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC
    } months;
    
    typedef struct {
        int days;
        char name[15];
    } month_entry;
    
    const month_entry month_list[12 + 1] = {
        { 0, ""},
        {31, "Enero"},   {28, "Febrero"},   {31, "Marzo"},
        {30, "Abril"},   {31, "Mayo"},      {30, "Junio"},
        {31, "Julio"},   {31, "Agosto"},    {30, "Septiembre"},
        {31, "Octubre"}, {30, "Noviembre"}, {31, "Diciembre"}
    };
    
    int main(void)
    {
        months month;
    
        for (month = JAN; month <= DEC; month++) {
            printf("%s %d días\n", month_list[month].name,
                                   month_list[month].days);
        }
    
        return 0;
    }

Ejemplo 18

  • Estructura mp_obj_str_t utilizada en MicroPython para almacenar cadenas de texto:

    typedef struct _mp_obj_str_t {
        mp_obj_base_t base;
        size_t hash;
        // len == number of bytes used in data, alloc = len + 1 because (at the moment) we also append a null byte
        size_t len;
        const byte *data;
    } mp_obj_str_t;

Ejemplo 18

  • Justifica el comportamiento distinto de las cadenas de texto en C y Python:

    #include <stdio.h>
    
    int main()
    {
        char text[] = "hola";
        printf("%s %p\n", text, text);
    
        text[0] = 'H';
        printf("%s %p\n", text, text);
    
        return 0;
    }
    text = "hola"
    print(f"{text} 0x{id(text):x}")
    
    text[0] = "H"  # ERROR
    
    text = text.replace("h", "H")
    print(f"{text} 0x{id(text):x}")

Evolución del lenguaje C