Manual de Programación en C Embebido

1. Introducción al C Embebido

La programación embebida se refiere al desarrollo de software para sistemas computacionales dedicados, generalmente con recursos limitados y funciones específicas.

Características clave:

Nota: El lenguaje C es el más utilizado en sistemas embebidos por su eficiencia, portabilidad y acceso a bajo nivel al hardware.

2. Diferencias entre C estándar y C embebido

2.1 Entorno de ejecución limitado

// En sistemas embebidos a menudo no hay sistema operativo
// Las funciones estándar como printf pueden no estar disponibles
// Se usan implementaciones reducidas o personalizadas

2.2 Acceso directo al hardware

// Ejemplo: Acceso a registros de un microcontrolador
#define PORT_A (*(volatile uint8_t *)0x40000000)

void main() {
    PORT_A = 0xFF;  // Escribe todos los bits del puerto A a 1
}

2.3 Optimización de recursos

// Uso de tipos de tamaño fijo para ahorrar memoria
#include <stdint.h>

int8_t smallVar;    // Entero de 8 bits con signo
uint16_t mediumVar; // Entero de 16 bits sin signo

3. Arquitectura de sistemas embebidos

3.1 Componentes principales

3.2 Modelo de memoria

// Estructura típica de memoria en un sistema embebido
/*
0x00000000 - 0x0000FFFF   Código (Flash)
0x20000000 - 0x2000FFFF   RAM
0x40000000 - 0x400FFFFF   Periféricos
*/

4. Herramientas de desarrollo

4.1 Toolchain

4.2 Entornos de desarrollo

4.3 Depuración

// Técnicas comunes:
// - Puntos de interrupción (breakpoints)
// - Depuración por printf (a través de UART)
// - LEDs de depuración
// - Analizadores lógicos

5. Sintaxis básica para sistemas embebidos

5.1 Punto de entrada

// En sistemas embebidos, el punto de entrada suele ser Reset_Handler
void Reset_Handler(void) {
    // Inicializa la memoria (.data, .bss)
    // Llama a main()
}

int main(void) {
    // Código de aplicación
    while(1) {
        // Bucle infinito (common en sistemas embebidos)
    }
}

5.2 Modificadores importantes

// volatile: Indica que la variable puede cambiar fuera del flujo del programa
volatile uint32_t systemTick;

// static: Mantiene la variable en memoria (no stack) y limita su alcance
static uint8_t internalCounter;

// const: Almacenamiento en flash para ahorrar RAM
const char welcomeMsg[] = "Sistema iniciado";

6. Manejo de periféricos

6.1 GPIO (Entrada/Salida de propósito general)

// Ejemplo: Configurar un pin GPIO como salida
#define LED_PORT    GPIOA
#define LED_PIN     5

void GPIO_Init(void) {
    // Habilita el reloj para GPIOA
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    
    // Configura el pin 5 como salida
    LED_PORT->MODER &= ~(3U << (LED_PIN * 2));  // Limpia bits
    LED_PORT->MODER |= (1U << (LED_PIN * 2));   // Modo salida
    
    // Tipo push-pull
    LED_PORT->OTYPER &= ~(1U << LED_PIN);
}

void toggleLED(void) {
    LED_PORT->ODR ^= (1U << LED_PIN);  // XOR para alternar el estado
}

6.2 UART (Comunicación serial)

// Ejemplo básico de transmisión UART
void UART_SendChar(uint8_t ch) {
    while(!(USART1->ISR & USART_ISR_TXE));  // Espera buffer vacío
    USART1->TDR = ch;
}

void UART_SendString(const char *str) {
    while(*str) {
        UART_SendChar(*str++);
    }
}

6.3 Temporizadores (Timers)

// Configuración básica de un timer
void TIM_Init(void) {
    // Habilita reloj para TIM2
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
    
    // Configura el prescaler para 1ms
    TIM2->PSC = SystemCoreClock / 1000 - 1;
    
    // Habilita interrupción por overflow
    TIM2->DIER |= TIM_DIER_UIE;
    
    // Habilita el timer
    TIM2->CR1 |= TIM_CR1_CEN;
    
    // Configura NVIC para TIM2
    NVIC_EnableIRQ(TIM2_IRQn);
}

void TIM2_IRQHandler(void) {
    if(TIM2->SR & TIM_SR_UIF) {
        TIM2->SR &= ~TIM_SR_UIF;  // Limpia flag de interrupción
        // Código a ejecutar cada 1ms
    }
}

7. Manejo de interrupciones

7.1 Conceptos básicos

7.2 Estructura típica de una ISR

void EXTI0_IRQHandler(void) {
    // Verifica que la interrupción proviene del pin correcto
    if(EXTI->PR & EXTI_PR_PR0) {
        // Limpia el flag de interrupción
        EXTI->PR = EXTI_PR_PR0;
        
        // Código a ejecutar en la interrupción
        toggleLED();
    }
}

7.3 Consideraciones importantes

Advertencia: Las variables compartidas entre una ISR y el código principal deben ser declaradas como volatile para evitar problemas de optimización.

8. Manejo de memoria

8.1 Tipos de memoria

8.2 Secciones de memoria

/* Estructura típica del linker script:
MEMORY
{
    FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K
    RAM (rwx)  : ORIGIN = 0x20000000, LENGTH = 32K
}

SECTIONS
{
    .text : { *(.text) } > FLASH
    .data : { *(.data) } > RAM AT> FLASH
    .bss  : { *(.bss) } > RAM
}*/

8.3 Gestión de memoria dinámica

Nota: En muchos sistemas embebidos se evita el uso de malloc/free debido a:

  • Fragmentación de memoria
  • Comportamiento no determinista
  • Overhead de gestión

Se prefieren estructuras estáticas o pools de memoria.

9. Modos de bajo consumo

9.1 Tipos de modos de bajo consumo

9.2 Ejemplo de implementación

void enterLowPowerMode(void) {
    // Configura un pin como fuente de wake-up
    EXTI->IMR |= EXTI_IMR_MR0;      // Habilita interrupción en línea 0
    EXTI->RTSR |= EXTI_RTSR_TR0;    // Trigger en flanco de subida
    
    // Configura el modo de bajo consumo
    PWR->CR |= PWR_CR_LPDS;         // Low-power deepsleep
    SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
    
    // Entra en modo de bajo consumo
    __WFI();  // Wait for interrupt
}

10. Sistemas Operativos en Tiempo Real (RTOS)

10.1 Conceptos básicos

10.2 Ejemplo con FreeRTOS

#include "FreeRTOS.h"
#include "task.h"

void vTask1(void *pvParameters) {
    while(1) {
        // Código de la tarea 1
        vTaskDelay(pdMS_TO_TICKS(100));  // Espera 100ms
    }
}

void vTask2(void *pvParameters) {
    while(1) {
        // Código de la tarea 2
        vTaskDelay(pdMS_TO_TICKS(500));  // Espera 500ms
    }
}

int main(void) {
    // Crea las tareas
    xTaskCreate(vTask1, "Task1", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
    xTaskCreate(vTask2, "Task2", configMINIMAL_STACK_SIZE, NULL, 2, NULL);
    
    // Inicia el scheduler
    vTaskStartScheduler();
    
    while(1);  // No debería llegar aquí
}

11. Buenas prácticas en C embebido

11.1 Código robusto

11.2 Optimización

11.3 Seguridad

12. Proyecto de ejemplo completo

12.1 Blinky con interrupciones

#include "stm32f1xx.h"

#define LED_PIN     GPIO_PIN_13
#define LED_PORT    GPIOC
#define BUTTON_PIN  GPIO_PIN_0
#define BUTTON_PORT GPIOA

volatile uint8_t buttonPressed = 0;

void GPIO_Init(void) {
    // Habilita relojes para GPIOA y GPIOC
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPCEN;
    
    // Configura PC13 como salida push-pull (LED)
    GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
    GPIOC->CRH |= GPIO_CRH_MODE13_1;  // Salida 2MHz
    
    // Configura PA0 como entrada con pull-up
    GPIOA->CRL &= ~(GPIO_CRL_CNF0 | GPIO_CRL_MODE0);
    GPIOA->CRL |= GPIO_CRL_CNF0_1;    // Entrada con pull-up/pull-down
    GPIOA->ODR |= GPIO_ODR_ODR0;      // Pull-up
}

void EXTI_Init(void) {
    // Habilita reloj para AFIO
    RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
    
    // Configura EXTI0 para PA0
    AFIO->EXTICR[0] &= ~AFIO_EXTICR1_EXTI0;
    AFIO->EXTICR[0] |= AFIO_EXTICR1_EXTI0_PA;
    
    // Configura trigger en flanco de bajada
    EXTI->FTSR |= EXTI_FTSR_TR0;
    
    // Habilita interrupción EXTI0
    EXTI->IMR |= EXTI_IMR_MR0;
    
    // Configura prioridad en NVIC
    NVIC_SetPriority(EXTI0_IRQn, 0);
    NVIC_EnableIRQ(EXTI0_IRQn);
}

void SysTick_Init(void) {
    SysTick->LOAD = SystemCoreClock/1000 - 1;  // 1ms
    SysTick->VAL = 0;
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | 
                    SysTick_CTRL_TICKINT_Msk | 
                    SysTick_CTRL_ENABLE_Msk;
}

void EXTI0_IRQHandler(void) {
    if(EXTI->PR & EXTI_PR_PR0) {
        EXTI->PR = EXTI_PR_PR0;  // Limpia flag
        buttonPressed = 1;
    }
}

int main(void) {
    GPIO_Init();
    EXTI_Init();
    SysTick_Init();
    
    uint32_t lastTick = 0;
    uint8_t ledState = 0;
    
    while(1) {
        if(buttonPressed) {
            buttonPressed = 0;
            ledState = !ledState;
            if(ledState) {
                LED_PORT->BSRR = LED_PIN;  // Enciende LED
            } else {
                LED_PORT->BRR = LED_PIN;   // Apaga LED
            }
        }
    }
}

13. Conclusión

La programación en C para sistemas embebidos requiere un enfoque diferente al desarrollo de aplicaciones de propósito general. Es fundamental comprender el hardware subyacente, las limitaciones de recursos y los requisitos de tiempo real.

Este manual cubre los conceptos básicos, pero cada sistema embebido es único. Siempre consulta la documentación específica del microcontrolador que estés utilizando.

14. Recursos adicionales