Guía Completa de Periféricos STM32 con Librería Estándar

Aunque las librerías HAL son ahora el método principal para desarrollar microcontroladores STM32, es inevitable que necesitemos usar la librería estándar en nuestro aprendizaje. En lugar de invertir tiempo en tutoriales en video, he compilado una guía de configuración de periféricos básicos utilizando la librería estándar para futuras referencias.

Plantilla de Proyecto y Árbol de Relojes STM32

1. Plantilla de Jiangxie Technology y Árbol de Relojes STM32

La carpeta Start contiene el archivo de inicio; elige el archivo de inicio adecuado para cada modelo. Los archivos 2-3 describen los registros del núcleo, el archivo 4 se encarga de configurar registros y sus direcciones correspondientes, y los archivos 5-6 se utilizan para la configuración del reloj. Asegúrate de agregar los archivos copiados a tu grupo en el proyecto seleccionando Add Files to Group XXX.

La carpeta User almacena tu código de usuario, con main.c como el punto de entrada principal. Otros archivos incluyen las relaciones de inclusión para las librerías de configuración y archivos de interrupción para funciones de manejo de interrupciones.

La carpeta Library está destinada a los archivos de la librería estándar.

La carpeta System se utiliza para las funciones de retardo.

Posteriormente, se añadirá una carpeta Hardware para los archivos de drivers de periféricos.

1.1 Agregar Rutas de Archivos de Cabecera

Utiliza la varita mágica (Option Bytes) y navega a C/C++ -> Include Paths.

1.2 Árbol de Relojes

El bus APB (Advanced Peripheral Bus) se utiliza para conectar varios periféricos. Generalmente, la frecuencia de APB2 es mayor que la de APB1.

En STM32, el sistema de reloj funciona como el pulso de un ser vivo, dictando la velocidad de operación del chip. El PLL (Phase-Locked Loop) actúa como una "bomba de aumento de presión" en este sistema, multiplicando la señal del oscilador de baja frecuencia para generar relojes de alto rendimiento.

1.2.1 Fuente de Señal: Oscilador (HSE)

Todo comienza con el oscilador de alta velocidad externo (HSE). En una placa de desarrollo STM32F103 típica, encontrarás un pequeño componente metálico etiquetado como 8.000.

  • Frecuencia: 8MHz.
  • Rol: Es la referencia física más estable, pero operando a 8MHz, el chip funcionaría muy lentamente.
1.2.2 Bomba de Aumento: PLL (Phase-Locked Loop)

Para alcanzar un alto rendimiento, necesitamos el PLL (Phase-Locked Loop).

  • Función: Multiplicación de frecuencia.
  • Fórmula de Cálculo: En la configuración estándar, primero divide el HSE por 1 (manteniendo 8MHz) y luego lo multiplica por 9 a través del PLL.
  • Resultado: 8MHz × 9 = 72MHz. Esta señal de 72MHz es nuestro SYSCLK (reloj del sistema).
1.2.3 Distribución: Relación de Frecuencia entre Buses y Periféricos

Una vez generado el reloj del sistema de 72MHz (SYSCLK), se distribuye a diferentes buses a través de "pre-divisores":

HCLK (Reloj de Bus de Alto Rendimiento - AHB)
  • Divisor: Generalmente 1 (SYSCLK / 1).
  • Frecuencia: 72MHz.
  • Periféricos Conectados: Memoria (Flash/RAM), DMA.
PCLK2 (Reloj de Periféricos de Alta Velocidad - APB2)
  • Divisor: Generalmente 1 (HCLK / 1).
  • Frecuencia: 72MHz.
  • Periféricos Correspondientes: GPIO, ADC, USART1, TIM1.
  • Nota: Los ADC tienen un divisor especial de 6 u 8 porque solo pueden operar hasta aproximadamente 14MHz.
PCLK1 (Reloj de Periféricos de Baja Velocidad - APB1)
  • Divisor: Obligatoriamente debe ser un divisor de HCLK, comúnmente configurado como 2 (HCLK / 2).
  • Frecuencia: 72MHz / 2 = 36MHz.
  • Periféricos Correspondientes: USART2/3, I2C, CAN, Timers Genéricos (TIM2-4).
1.2.4 Resumen: Tabla de Niveles de Frecuencia
Nombre del Reloj Fuente de Señal Frecuencia Típica Relación Lógica
HSE Oscilador Externo 8MHz Entrada Original
PLLCLK Coeficiente Multiplicador HSE 72MHz Salida del PLL
SYSCLK PLLCLK 72MHz Frecuencia Principal del Chip
PCLK2 SYSCLK / 1 72MHz Para periféricos APB2
PCLK1 SYSCLK / 2 36MHz Para periféricos APB1
  1. Entrada/Salida GPIO


// Habilitar el reloj del bus APB2 para el puerto GPIOA
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

// Definir la estructura de inicialización de GPIO
GPIO_InitTypeDef GPIO_InitStructure;

// Configurar el modo de funcionamiento del pin (Salida Push-Pull)
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
// Definir el pin específico a configurar (Pin 0)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
// Configurar la velocidad del pin (50MHz)
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

// Inicializar el pin GPIO A con la configuración definida
GPIO_Init(GPIOA, &GPIO_InitStructure);
   

// Poner el pin PA0 en estado bajo (LOW)
GPIO_ResetBits(GPIOA, GPIO_Pin_0);

// Poner el pin PA0 en estado alto (HIGH)
GPIO_SetBits(GPIOA, GPIO_Pin_0);
   
  1. Interrupciones Externas

Interrupción: Durante la ejecución del programa principal, si se cumplen ciertas condiciones de activación (fuente de interrupción), la CPU pausa el programa actual para ejecutar una rutina de interrupción. Una vez completada, regresa al punto de pausa para continuar la ejecución.

Prioridad de Interrupción: Cuando múltiples fuentes de interrupción solicitan atención simultáneamente, la CPU las prioriza basándose en su urgencia, atendiendo primero a las más críticas.

Anidamiento de Interrupciones: Si una rutina de interrupción se está ejecutando y ocurre una nueva interrupción de mayor prioridad, la CPU pausa la rutina actual para atender la nueva interrupción. Al finalizar, retorna a la rutina previamente pausada.

  • Existen 68 canales de interrupción enmascarables, que cubren múltiples periféricos como EXTI, TIM, ADC, USART, SPI, I2C, RTC, etc.
  • El NVIC (Nested Vectored Interrupt Controller) gestiona centralmente las interrupciones. Cada canal de interrupción dispone de 16 niveles de prioridad programables. Se pueden agrupar las prioridades, configurando además prioridades de pre-emptión y de sub-prioridad. La prioridad de pre-emptión determina el anidamiento de interrupciones, mientras que la sub-prioridad gestiona la cola de ejecución.
  • Un vector de interrupción es esencialmente una dirección (un puntero) que apunta al inicio de la función de servicio de interrupción (ISR) en memoria. Dado que STM32 tiene muchas interrupciones (reset, NMI, varias interrupciones de periféricos), la CPU necesita una lista para gestionarlas: la tabla de vectores de interrupción.
  • AFIO se utiliza principalmente para la reconfiguración de pines (multiplexing) y la configuración de pines de interrupción.
  • La secuencia de control es: GPIO -> AFIO -> EXTI -> NVIC -> CPU.

// Habilitar el reloj para el puerto GPIOB y AFIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

// Configuración de GPIO como entrada con pull-up
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // Entrada con resistencia pull-up interna
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);

// Mapeo de la línea de interrupción externa a un pin GPIO
// AFIO configura la línea de interrupción externa: Pin PB14 se conecta a EXTI14
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);

// Configuración de la línea de interrupción externa
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line14;        // Seleccionar la línea EXTI14
EXTI_InitStructure.EXTI_LineCmd = ENABLE;        // Habilitar la línea EXTI14
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // Modo de interrupción (puede ser modo evento)
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // Disparar en flanco de bajada
EXTI_Init(&EXTI_InitStructure);

// Configurar el grupo de prioridades del NVIC
// Se utiliza el Grupo 2 (2 bits para prioridad de pre-emptión, 2 bits para sub-prioridad)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

// Configuración de la interrupción en el NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; // Canal de interrupción para EXTI14 (pertenece al grupo EXTI15_10)
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // Prioridad de pre-emptión
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;      // Sub-prioridad
NVIC_Init(&NVIC_InitStructure);
   

// Rutina de servicio de interrupción para EXTI15_10
void EXTI15_10_IRQHandler(void)
{
   // Verificar si el bit de interrupción pendiente para EXTI_Line14 está activo
   if (EXTI_GetITStatus(EXTI_Line14) == SET)
   {
       // Si se observa un comportamiento errático de datos, se puede verificar el nivel del pin nuevamente
       // para evitar el rebote.
       if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0)
       {
           // Incrementar un contador (ejemplo: CountSensor_Count)
           CountSensor_Count++;
       }
       // Limpiar el bit de interrupción pendiente para EXTI_Line14
       EXTI_ClearITPendingBit(EXTI_Line14);
   }
}
   

En resumen, STM32 utiliza 4 bits para representar la prioridad de una interrupción. Estos 4 bits se dividen en dos partes: Prioridad de Pre-emptión y Sub-prioridad.

3.1 Diferencia entre las Dos Prioridades
  • Prioridad de Pre-emptión (bits de mayor peso):
    • Puede interrumpir a otras. Si la prioridad de pre-emptión de la interrupción A es mayor que la de la interrupción B, y B se está ejecutando, cuando ocurre A, la CPU detendrá inmediatamente B para ejecutar A (esto es el "anidamiento").
  • Sub-prioridad (bits de menor peso):
    • Gestión de cola. Si dos interrupciones tienen la misma prioridad de pre-emptión y ocurren simultáneamente, la que tenga mayor sub-prioridad (valor numérico menor) se ejecutará primero.
  • Nota: La sub-prioridad no puede interrumpir una interrupción en curso. Si las prioridades de pre-emptión son iguales, incluso si la sub-prioridad es mayor, se debe esperar a que la ejecución de la otra interrupción finalice.
3.2 Agrupación de Prioridades (Grouping)

Dado que solo hay 4 bits en total, debes decidir cuántos bits se dedicarán a "pre-emptión" y cuántos a "sub-prioridad". STM32 ofrece 5 métodos de agrupación:

Método de Agrupación Bits de Prioridad de Pre-emptión (Ancho de bits) Bits de Sub-prioridad (Ancho de bits) Rango de Prioridad de Pre-emptión Rango de Sub-prioridad
Grupo 0 0 bits 4 bits 0 (sin pre-emptión) 0 - 15
Grupo 1 1 bit 3 bits 0 - 1 0 - 7
Grupo 2 2 bits 2 bits 0 - 3 0 - 3
Grupo 3 3 bits 1 bit 0 - 7 0 - 1
Grupo 4 4 bits 0 bits 0 - 15 0 (sin sub-prioridad)
3.3 Regla Importante: Valor Numérico Menor Implica Mayor Prioridad

Independientemente del tipo de prioridad, 0 es la prioridad más alta y 15 la más baja. Es similar a la numeración de un equipo de élite, donde el número 001 siempre es más importante que el 015.

3.4 ¿Por Qué Configurar la Agrupación Primero en el Código?

En la librería estándar (STM32F10x_StdPeriph_Driver), normalmente verás esta línea al principio de la función main(): NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

¿Por qué escribir esto primero?

  1. Globalidad: La agrupación de prioridades solo se puede configurar una vez en un proyecto. Una vez que se decide usar "2 bits de pre-emptión + 2 bits de sub-prioridad", todos los periféricos posteriores (UART, Timers, etc.) deben seguir esta regla.

  2. Evitar Confusiones: Si no configuras la agrupación, el sistema tendrá un valor predeterminado. Si configuras la prioridad para el inicializador del puerto serie usando el "Grupo 2" y para el inicializador del temporizador usando el "Grupo 3", la lógica del sistema puede fallar de manera impredecible.

  3. Temporizadores (Timers)


En el desarrollo STM32, los TIM (Timers/Temporizadores) son uno de los periféricos más potentes y de uso más frecuente. No solo funcionan como "relojes de alarma" para la temporización, sino que también son herramientas clave para generar formas de onda PWM (control de motores, servos) y medir frecuencias de entrada (interfaz de codificador).

Los temporizadores del STM32F103 se clasifican en tres tipos según su complejidad funcional: Temporizadores Avanzados, Temporizadores Genéricos y Temporizadores Básicos. La fuente de reloj de un temporizador puede ser interna o externa (los flancos de un GPIO también pueden actuar como fuente de reloj).

Clasificación de Temporizadores

Tipo Instancia Funciones Principales
Temporizador Avanzado TIM1, TIM8 Incluye todas las funciones de los temporizadores genéricos, más generación de tiempo muerto, salida complementaria y entrada de freno (diseñado para control de motores). Conectado al bus APB2.
Temporizador Genérico TIM2, TIM3, TIM4, TIM5 Capacidades de temporización, comparación de salida (PWM), captura de entrada (medición de frecuencia/ciclo de trabajo), cascada, etc. Conectado al bus APB1.
Temporizador Básico TIM6, TIM7 Solo funciones de temporización básicas, sin canales externos. Comúnmente utilizado para disparar el DAC. Conectado al bus APB1.

4.1 Interrupción de Temporizador


void Timer_Init(void)
{
   // Habilitar el reloj para el temporizador TIM2 en el bus APB1
   RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

   // Configurar el TIM2 para usar su reloj interno (esta línea puede omitirse ya que es el valor por defecto)
   TIM_InternalClockConfig(TIM2);
   // Para usar un reloj externo, descomentar y configurar:
   // TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x00);

   // Estructura de configuración de la base de tiempo del temporizador
   TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;

   // Configurar el divisor de reloj del temporizador (sin división)
   TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
   // Modo de contador: ascendente
   TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
   // Período del temporizador (ARR): 10000 - 1 = 9999
   TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;
   // Prescaler (PSC): 7200 - 1 = 7199. Frecuencia del contador = 72MHz / (7199 + 1) = 10kHz
   TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;
   // Contador de repetición (para temporizadores avanzados), establecido en 0 para temporizadores genéricos
   TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
   // Inicializar la base de tiempo del TIM2
   TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);

   // Limpiar manualmente la bandera de actualización de interrupción para evitar una interrupción inmediata
   TIM_ClearFlag(TIM2, TIM_FLAG_Update);
   // Habilitar la interrupción de actualización del TIM2
   TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);

   // Configurar el grupo de prioridades del NVIC (Grupo 2)
   NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

   // Estructura de configuración del NVIC
   NVIC_InitTypeDef NVIC_InitStructure;
   // Seleccionar el canal de interrupción para TIM2
   NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
   NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
   // Establecer la prioridad de pre-emptión y sub-prioridad
   NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
   NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
   // Inicializar el NVIC
   NVIC_Init(&NVIC_InitStructure);

   // Habilitar el temporizador TIM2
   TIM_Cmd(TIM2, ENABLE);
}

// Rutina de servicio de interrupción para TIM2
void TIM2_IRQHandler(void)
{
   // Verificar si el bit de interrupción de actualización está activo
   if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
   {
       // Limpiar la bandera de interrupción de actualización
       TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
       // Aquí iría el código a ejecutar cada vez que ocurra la interrupción del temporizador
   }
}
   

Los parámetros de los temporizadores STM32 tienen dos capas de registros:

  • Registro de Precarga (Preload Register): Donde escribes directamente a través del código (ej. TIM_TimeBaseInit), como un "suplente".
  • Registro Sombra (Shadow Register): Donde el hardware realmente cuenta, como un "jugador oficial".

Lógica Central: Solo cuando ocurre un Evento de Actualización (Update Event), el hardware copia el valor del "suplente" al "jugador oficial".

Sin este mecanismo, si el temporizador está contando a alta velocidad y modificas el ARR (Auto-Reload Register) a un valor menor que el contador actual, el contador podría "perderse" (nunca alcanzar el valor ARR), causando un bloqueo del sistema.

¿Por qué se entra inmediatamente en interrupción al inicializar? Porque dentro de la función de librería estándar TIM_TimeBaseInit, para que los valores PSC y ARR que has escrito tengan efecto inmediato, se incluye la línea: TIMx->EGR = TIM_PSCReloadMode_Immediate;. Esto dispara manualmente un evento de actualización de software (bit UG). Como has habilitado la interrupción de actualización, el hardware interpreta esto como una interrupción y ejecuta la rutina correspondiente. La solución es limpiar manualmente la bandera de interrupción después de TIM_TimeBaseInit: TIM_ClearFlag(TIM2, TIM_FLAG_Update);.

4.2 Comparación de Salida (Output Compare)


void PWM_Init(void)
{
   // Habilitar el reloj para TIM2 (APB1) y GPIOA (APB2)
   RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
   RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

   // Opcional: Mapeo de pines para TIM2 si no se usan los predeterminados
   // RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
   // GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE); // Ejemplo de remapeo parcial
   // GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); // Deshabilitar JTAG para liberar pines

   // Configuración del pin GPIO A como salida de función alternativa Push-Pull
   GPIO_InitTypeDef GPIO_InitStructure;
   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // Función alternativa Push-Pull
   GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;       // Pin PA0 (TIM2 Channel 1 predeterminado)
   GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
   GPIO_Init(GPIOA, &GPIO_InitStructure);

   // Configuración del reloj interno para TIM2
   TIM_InternalClockConfig(TIM2);

   // Configuración de la base de tiempo del temporizador
   TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
   TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
   TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
   TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;       // ARR: 99 (frecuencia base = 72MHz / (719+1) / 100 = 1kHz)
   TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1;    // PSC: 719 (frecuencia de conteo = 72MHz / (720) = 100kHz)
   TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
   TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);

   // Configuración de la comparación de salida (OC)
   TIM_OCInitTypeDef TIM_OCInitStructure;
   // Inicializar la estructura OC con valores predeterminados
   TIM_OCStructInit(&TIM_OCInitStructure);
   // Modo de comparación de salida: PWM Mode 1
   TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
   // Polaridad de salida: HIGH (el pin se pone en HIGH cuando CNT < CCR)
   TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
   // Estado de salida: Habilitado
   TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
   // Valor de pulso (CCR): 0 (inicialmente el ciclo de trabajo es 0%)
   TIM_OCInitStructure.TIM_Pulse = 0;
   // Inicializar el Canal 1 de comparación de salida del TIM2
   TIM_OC1Init(TIM2, &TIM_OCInitStructure);

   // Habilitar el temporizador TIM2
   TIM_Cmd(TIM2, ENABLE);
}

// Función para establecer el valor de comparación del Canal 1
void PWM_SetCompare1(uint16_t Compare)
{
   TIM_SetCompare1(TIM2, Compare);
}
   

En el modo de comparación de salida, el hardware compara continuamente dos valores:

  • CNT (Counter): El contador que incrementa constantemente.
  • CCR (Capture/Compare Register): El "valor de comparación" que configuras manualmente.

Cuando la relación entre ellos cambia (igual, mayor o menor), el nivel del pin GPIO correspondiente se invierte según el modo preestablecido.

STM32 proporciona varios modos para manejar estos "resultados de comparación", los más comunes son:

  • Modo PWM 1/2: El modo más utilizado, para generar señales de modulación por ancho de pulso (control de motores, LEDs, servos).
  • Toggle (Conmutar): Cuando CNT=CCR, el nivel del pin cambia. Comúnmente usado para generar ondas cuadradas precisas.
  • Set/Reset (Establecer/Reiniciar): Cuando hay coincidencia, fuerza el pin a un nivel alto o bajo.
  • Frozen (Congelado): El nivel del pin permanece sin cambios al coincidir.

4.3 Captura de Entrada (Input Capture)

¡Claro! Esta es una forma muy eficiente. Ver el código primero te ayuda a visualizar la estructura del proyecto, y luego la explicación detallada te permitirá comprender los principios a fondo.

Ya que hablamos de Captura de Entrada (Input Capture), la usaré para el escenario de "medir la frecuencia de un pulso externo". Primero te mostraré el código de configuración de la librería estándar, y luego lo desglosaré en detalle.

Código de Configuración de Captura de Entrada (Medición de Frecuencia)

Este código configuar TIM3\_CH1 (pin PA6) en modo de captura de entrada para medir la frecuencia de una señal externa.


#include "stm32f10x.h"

void IC_Init(void)
{
   GPIO_InitTypeDef GPIO_InitStructure;
   TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
   TIM_ICInitTypeDef  TIM_ICInitStructure;

   // 1. Habilitar relojes (TIM3 en APB1, GPIOA en APB2)
   RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
   RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

   // 2. Configuración GPIO: PA6 como modo de entrada (flotante o con pull-up)
   GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // Modo de entrada flotante
   GPIO_Init(GPIOA, &GPIO_InitStructure);

   // 3. Configuración de la base de tiempo del temporizador (frecuencia principal 72MHz)
   // Configuramos ARR al máximo para evitar desbordamientos antes de la segunda captura.
   TIM_TimeBaseStructure.TIM_Period = 65535;          // ARR (Auto-Reload Register)
   TIM_TimeBaseStructure.TIM_Prescaler = 71;          // PSC (Prescaler): Divide por 72, frecuencia del contador = 1MHz
   TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
   TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
   TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

   // 4. Configuración del canal de captura de entrada (TIM3_CH1)
   TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;          // Seleccionar Canal 1
   TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; // Capturar en flanco de subida
   TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; // Modo de conexión directa
   TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;     // Sin pre-escalado, captura en cada flanco
   TIM_ICInitStructure.TIM_ICFilter = 0x0F;                  // Filtro (0x0 a 0xF, mayor valor = filtro más fuerte)
   TIM_ICInit(TIM3, &TIM_ICInitStructure);

   // Función clave: Configura automáticamente el otro canal para polaridad opuesta (flanco de bajada) y conexión cruzada.
   // Solo se debe usar una de las dos: TIM_ICInit o TIM_PWMIConfig.
   TIM_PWMIConfig(TIM3, &TIM_ICInitStructure);

   // --- Paso Clave: Configurar el modo esclavo para reset automático ---

   // 4. Seleccionar la fuente de trigger: TI1FP1 (señal del canal 1 filtrada)
   TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);

   // 5. Seleccionar el modo esclavo: Modo Reset (Reset Mode)
   // Efecto: Cada vez que TI1FP1 detecta un flanco de subida, el contador CNT se pone a cero automáticamente.
   TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);

   // 5. Habilitar el temporizador
   TIM_Cmd(TIM3, ENABLE);
}

// Debido a razones de hardware, se suma 1.
uint32_t IC_GetFreq(void)
{
   // Frecuencia = 1MHz / (Valor capturado en CCR1 + 1)
   return 1000000 / (TIM_GetCapture1(TIM3) + 1);
}

uint32_t IC_GetDuty(void)
{
   // Ciclo de trabajo = (Tiempo en nivel alto + 1) * 100 / (Período total + 1)
   // CCR2 captura el tiempo en nivel alto (usando la configuración PWM)
   return (TIM_GetCapture2(TIM3) + 1) * 100 / (TIM_GetCapture1(TIM3) + 1);
}
   

(1). ¿Por qué el modo GPIO es IN_FLOATING?

En "Comparación de Salida", usamos AF_PP (Función Alternativa Push-Pull) porque el temporizador controla el pin. En "Captura de Entrada", la señal externa impulsa el pin, por lo que debe configurarse como entrada. La entrada flotante refleja la forma de onda externa de manera más fiel.

(2). Función del Filtro (Filter)

TIM_ICFilter = 0x0F. En circuitos reales, los flancos de señal pueden tener "ruido" (glitches). El filtro funciona así: solo si el nivel del pin se mantiene estable durante N ciclos de reloj consecutivos, el temporizador lo considera un flanco válido. Esto mejora enormemente la estabilidad de la medición de señales de alta frecuencia o ruidosas.

(3). Conexión Directa (DirectTI) vs. Indirecta (IndirectTI)

  • DirectTI: El canal 1 captura la señal del pin 1.
  • IndirectTI: El canal 1 captura la señal del pin 2. Esto permite usar una señal de entrada en un pin y capturarla simultáneamente con dos canales (por ejemplo, uno para el período y otro para el ciclo de trabajo).

(4). ¿Cómo se calcula la frecuencia?

Una vez que el código se ejecuta, necesitas leer dos valores de captura:

  1. Primer flanco de subida: Lee CCR1, lo llamamos N1.
  2. Segundo flanco de subida: Lee CCR1, lo llamamos N2.
  3. Cálculo: Número de pulsos entre las dos capturas M = N2 - N1 (si N2 < N1, indica que el ARR se desbordó una vez, hay que sumar 65536).
  4. Frecuencia Final: f = fcnt / M (donde fcnt es la frecuencia de conteo configurada en PSC, en este caso 1MHz).

Esta es la "ruta neuronal" central del modo PWM/IC. TI1FP1 y TI1FP2 (Timer Input 1 Filtered Prescaled) son las señales que, después de ser procesadas, entran al temporizador.

En el modo PWM/IC, se conectan de forma cruzada, permitiendo que la señal de un pin impulse dos canales de captura simultáneamente.

Código Clave de Configuración PWMI (para TI1FP)


// 1. Seleccionar la fuente de entrada del trigger: TI1FP1
// Esto indica al controlador del modo esclavo: monitoriza la señal filtrada del Canal 1.
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);

// 2. Configurar el modo esclavo: Modo Reset
// Tan pronto como TI1FP1 detecta un flanco de subida, CNT se pone a cero.
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);

// 3. Configuración de polaridad (automáticamente manejada por TIM_PWMIConfig)
// TI1FP1 -> Disparador en flanco de subida (captura período, dispara reset)
// TI1FP2 -> Disparador en flanco de bajada (captura duración del nivel alto, no dispara reset)
   

Explicación Detallada

La zona azul inferior derecha del diagrama muestra el flujo de la señal:

(1). Procesamiento de TI1 (Señal Fuente)

La señal original que entra por el pin GPIO se llama TI1. Primero pasa por dos "puntos de control":

  • Filtro: Elimina el ruido.
  • Detección de Flanco / Selección de Polaridad: Decide si se monitoriza el flanco de subida o de bajada.

La señal procesada se convierte en T1F (Filtered).

(2). TI1FP1: Ruta Principal

  • Definición: Señal del Canal 1 después de filtrado.
  • Función A (Captura): Se conecta a CCR1. Al llegar el flanco de subida, CCR1 = CNT (registra el período total).
  • Función B (Trigger): Es la fuente de trigger para el controlador del modo esclavo. Como se indica en el código TIM_TS_TI1FP1, es la que da la orden al "modo esclavo (Reset)": "¡El período ha comenzado, pon CNT a cero!"

(3). TI1FP2: Ruta Auxiliar (Canal Cruzado)

  • Definición: Proviene del Canal 1, pero se redirige al circuito de captura del Canal 2.
  • Función: En la función TIM_PWMIConfig, se configura con polaridad opuesta (flanco de bajada).
  • Lógica: Al llegar el flanco de bajada, dispara CCR2 = CNT. Dado que CNT ha estado contando desde el flanco de subida (punto 0), el valor en CCR2 representa la duración del nivel alto.

Resumen

Nombre de la Señal Objetivo Correspondiente Rol en PWMI
TI1FP1 CCR1 & Controlador de Modo Esclavo Gestor del Período: Responsable de registrar el tiempo total y comandar el reset de CNT.
TI1FP2 CCR2 Medidor de Duración: Responsable de marcar el punto intermedio y registrar la duración del nivel alto.

Este diseño resuelve ingeniosamente un problema: ¿cómo medir dos dimensiones de tiempo simultáneamente con un solo pin? La respuesta es "dividir" la señal TI1 en dos: TI1FP1 para el período y TI1FP2 para el ancho de pulso.

4.4 Interfaz de Codificador (Encoder Interface)

En STM32, el Modo de Interfaz de Codificador (Encoder Interface Mode) es una "tecnología negra" a nivel de hardware de los temporizadores genéricos. Permite conectar directamente codificadores incrementales en cuadratura, y el hardware maneja automáticamente la diferencia de fase de las señales A y B para lograr conteo automático ascendente/descendente.

Código de Configuración de Modo Codificador

Este código configura TIM3 en modo codificador para CH1 (PA6) y CH2 (PA7).


void Encoder_Init(void)
{
   GPIO_InitTypeDef GPIO_InitStructure;
   TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;

   // 1. Habilitar relojes
   RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
   RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

   // 2. Configuración GPIO: PA6/PA7 como modo de entrada
   GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // Se recomienda entrada con pull-up para evitar rebotes de pines flotantes
   GPIO_Init(GPIOA, &GPIO_InitStructure);

   // 3. Configuración de la base de tiempo del temporizador
   // Para modo codificador, usualmente se configura ARR al máximo (65535),
   // a menos que necesites un reset automático al llegar a un valor específico.
   TIM_TimeBaseStructure.TIM_Period = 65535;
   TIM_TimeBaseStructure.TIM_Prescaler = 0;           // En modo codificador, generalmente no se usa pre-escalador para capturar bordes directamente.
   TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
   TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
   TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

   // 4. Configuración de la interfaz del codificador
   // TIM_EncoderMode_TI12: Cuenta en todos los flancos de subida/bajada de las fases A y B (precisión 4x).
   // Los dos últimos parámetros son para la polaridad de entrada (si se invierten).
   TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12,
                              TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);

   // 5. Habilitar el temporizador
   TIM_Cmd(TIM3, ENABLE);
}

// Para obtener la posición o velocidad actual en el código:
// int16_t position = (int16_t)TIM_GetCounter(TIM3);
   

Explicación Detallada

1. Lógica de Señales en Cuadratura

El codificador genera dos señales: Fase A (TI1) y Fase B (TI2). Estas señales tienen una diferencia de fase de 90∘.

  • Rotación en sentido horario: La fase A lidera a la fase B.
  • Rotación en sentido antihorario: La fase B lidera a la fase A.

2. ¿Cómo Determina la Dirección el Hardware?

Una vez que llamas a TIM_EncoderInterfaceConfig, la lógica interna del hardware del temporizador detecta automáticamente:

  • Si se detecta que A lidera a B, el contador CNT incrementa.
  • Si se detecta que B lidera a A, el contador CNT decrementa.

Todo esto sucede sin intervención de la CPU. Incluso si el motor gira muy rápido, el hardware puede capturar con precisión cada cambio.

3. Concepto de Multiplicación de Frecuencia (Mejora de Precisión)

En la configuración de la función, puedes elegir entre tres modos:

  • TIM\_EncoderMode\_TI1: Cuenta solo en los flancos de la fase A (precisión x2).
  • TIM\_EncoderMode\_TI2: Cuenta solo en los flancos de la fase B (precisión x2).
  • TIM\_EncoderMode\_TI12: Cuenta en todos los flancos de subida y bajada de A y B.
  • Efecto: Si el codificador tiene 500 líneas físicas, con el modo TI12, obtendrás 500×4 = 2000 pulsos por revolución. Esto no solo mejora la precisión de la posición, sino que también filtra eficazmente las pequeñas oscilaciones en los pines (ya que una oscilación no genera un cambio de fase ortogonal completo).

4. Manejo de Desbordamientos de 16 bits

Dado que el TIM3 es un temporizador de 16 bits, el rango de CNT es de 0 a 65535.

  • Truco: Convierte el valor leído de uint16_t a int16_t.
  • Beneficio: Si el contador se decrementa desde 0 hasta 65535, al convertirlo a int16_t se convierte en -1. De esta manera, incluso si el motor gira en sentido contrario cruzando el punto cero, tu lógica de software seguirá reconociendo correctamente el cambio de posición.

Resumen

Función Conteo de Pulsos con Interrupción Externa Tradicional Modo Codificador del Temporizador
Uso de CPU Muy Alto (cada pulso requiere una interrupción) Muy Bajo (conteo automático por hardware)
Identificación de Dirección Requiere lógica de software compleja para determinar la fase Identificación automática de aumento/disminución por hardware
Resistencia a Interferencias Fácilmente afectado por pulsos erróneos La lógica de hardware filtra inherentemente señales no ortogonales
Escenarios de Aplicación Conteo lento, conteo simple Control de bucle cerrado de motores, posicionamiento preciso
  1. Convertidor Analógico-Digital (ADC)

5.1 ADC de Canal Único

Código de Configuración ADC

Ejemplo de configuración para el ADC1 Canal 0 (PA0) del STM32F103 en modo de conversión única:


void AD_Init(void)
{
   // Habilitar el reloj para ADC1 (APB2) y GPIOA (APB2)
   RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
   RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

   // Configurar el reloj del ADC: PCLK2 dividido por 6.
   // Si PCLK2 es 72MHz (predeterminado), el reloj del ADC será 12MHz, cumpliendo el requisito de no exceder 14MHz.
   RCC_ADCCLKConfig(RCC_PCLK2_Div6);

   // Configuración del pin GPIO A como entrada analógica
   GPIO_InitTypeDef GPIO_InitStructure;
   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // Modo de entrada analógica
   GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
   GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
   GPIO_Init(GPIOA, &GPIO_InitStructure);

   // Configurar el canal regular del ADC1: Canal 0, secuencia 1, tiempo de muestreo 55.5 ciclos
   ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);

   // Configuración general del ADC
   ADC_InitTypeDef ADC_InitStructure;
   ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;  // Modo independiente: ADC1 y ADC2 no interfieren entre sí
   ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;  // Alineación a la derecha: el resultado de conversión se almacena en los 12 bits inferiores del registro, facilitando la lectura directa.
   ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // Disparo por software: la conversión se inicia por comando de software.
   ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // Modo de conversión única: se detiene después de una conversión, esperando el próximo disparo.
   ADC_InitStructure.ADC_ScanConvMode = DISABLE;   // Modo no escaneo: solo un canal participa en la conversión.
   ADC_InitStructure.ADC_NbrOfChannel = 1; // Número de canales regulares: 1 (solo ADC_Channel_0 participa en la conversión).
   ADC_Init(ADC1, &ADC_InitStructure);

   // Habilitar el ADC1
   ADC_Cmd(ADC1, ENABLE);

   // Calibración del ADC
   ADC_ResetCalibration(ADC1); // Reiniciar la calibración
   while (ADC_GetResetCalibrationStatus(ADC1) == SET); // Esperar a que se complete el reinicio
   ADC_StartCalibration(ADC1); // Iniciar la calibración
   while (ADC_GetCalibrationStatus(ADC1) == SET); // Esperar a que se complete la calibración
}

// Función para obtener el valor de conversión ADC
uint16_t AD_GetValue(void)
{
   // Iniciar la conversión por software
   ADC_SoftwareStartConvCmd(ADC1, ENABLE);
   // Esperar a que la conversión finalice (bandera EOC - End Of Conversion)
   while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);
   // Obtener el valor de conversión
   return ADC_GetConversionValue(ADC1);
}
   

Explicación Detallada

ADC (Analog-to-Digital Converter): Convierte voltajes analógicos continuos (como la salida de un sensor) en valores digitales que la CPU puede procesar.

(1). Resolución (Resolution)

  • El ADC del STM32F103 es de 12 bits.
  • Significado: Divide el rango de voltaje de entrada (típicamente 0V a 3.3V) en 212 = 4096 partes iguales.
  • Cálculo: Si se lee el valor digital 2048, el voltaje correspondiente es 3.3V × (2048 / 4096) = 1.65V.

(2). Reloj del ADC y Tiempo de Muestreo

  • El ADC está conectado al bus APB2, pero tiene su propio pre-divisor ADC.
  • Límite: La frecuencia de operación del ADC no debe exceder los 14MHz. Si la frecuencia principal es 72MHz, se suele elegir un divisor de 6 (12MHz) o 8 (9MHz).
  • Tiempo de Muestreo: El voltaje no se lee instantáneamente; el ADC necesita un corto tiempo para cargar el condensador interno. Un tiempo de muestreo más largo resulta en mediciones más precisas, pero más lentas.

(3). Modos de Conversión

  • Conversión Única (Single): Dispara una vez, convierte una vez y se detiene. Adecuado para monitorización de voltaje con baja exigencia de tiempo real.
  • Conversión Continua (Continuous): Una vez disparado, realiza conversiones continuamente. La CPU puede leer el valor más reciente en cualquier momento.
  • Modo de Escaneo (Scan): Si hay múltiples canales (ej., medir temperatura, voltaje, luz simultáneamente), los convierte secuencialmente.

(4). Grupo Regular vs. Grupo Inyectado

  • Grupo Regular (Regular Group): Como una línea de producción normal, soporta hasta 16 canales.
    • Problema: En modo escaneo, el resultado de CH1 sobrescribirá el de CH0.
    • Solución: El escaneo del grupo regular debe combinarse con DMA para mover los datos antes de que sean sobrescritos.
  • Grupo Inyectado (Injected Group): Como una "cola prioritaria", puede interrumpir la conversión del grupo regular y procesar señales urgentes de forma prioritaria.
    • Ventaja: Tiene cuatro registros de datos independientes (ADC_JDR1 a ADC_JDR4). Después de convertir 4 canales, cada uno tiene su "cajón" propio, sin sobrescrituras, permitiendo una fácil lectura sin DMA.

(5). Alineación de Datos

  • Alineación a la Derecha (Right Align): Los datos de 12 bits se colocan en los 12 bits inferiores de un registro de 16 bits. El valor leído es directamente 0-4095.
  • Alineación a la Izquierda (Left Align): Los datos se colocan en los 12 bits superiores. Comúnmente usado cuando solo se necesita precisión de 8 bits, tomando directamente el byte superior.
  1. DMA (Acceso Directo a Memoria)

Clasificación de Memoria y Resumen de Configuración


/* Distribución típica de memoria del STM32F103 (concepto lógico) */
#define FLASH_START  0x08000000  // Dirección de inicio de la memoria de programa
#define SRAM_START   0x20000000  // Dirección de inicio de la memoria RAM
#define PERIPH_START 0x40000000  // Direcciones de registros de periféricos (ADC, TIM, GPIO, etc.)

/* En el código, forzar una variable a una región de memoria específica (Ejemplo Keil) */
uint32_t const table[] __attribute__((at(0x0800F000))) = {1, 2, 3}; // Almacenado en Flash
uint32_t data_buf[100] __attribute__((section("SRAM_POOL")));      // Almacenado en SRAM
   

Explicación Detallada: Los Cuatro Pilares de la Memoria del Microcontrolador

1. Flash (Memoria de Programa)

  • Flash es el "disco duro" del microcontrolador, es no volátil.
  • Uso: Almacena el código binario compilado (Code) y constantes (RO-data).
  • Características: No se pierde al apagar, lectura rápida, pero escritura (programación) lenta y vida útil limitada.
  • Punto Clave: En STM32, Flash generalmente se mapea desde 0x08000000. Cada línea de tu código se convertirá finalmente en carga eléctrica almacenada aquí.

2. SRAM (Memoria Estática de Acceso Aleatorio)

  • SRAM es la "memoria RAM" del microcontrolador, es volátil.
  • Uso: Almacena variables (RW-data, ZI-data), Heap y Stack.
  • Características: Lectura y escritura extremadamente rápidas, pero los datos se pierden inmediatamente al apagar.
  • Particiones Lógicas:
    • Stack: Almacena variables locales, protección del contexto durante llamadas a funciones.
    • Heap: Utilizado para asignación de memoria dinámica con malloc.

3. Registros (Registers)

  • Los registros son las unidades de almacenamiento más rápidas dentro de la CPU.
  • Registros de Propósito General: Como R0-R15 en ARM, utilizados para operaciones matemáticas y saltos lógicos.
  • Registros de Funciones Especiales (SFR): Son los que más conoces, como ADC_DR, TIMx_CNT. Aunque tienen mapeo de direcciones, son esencialmente interruptores que controlan el hardware.

4. EEPROM / Registros de Respaldo

  • EEPROM: Algunos microcontroladores la tienen integrada para almacenar parámetros que se modifican con frecuencia y no deben perderse al apagar (ej., coeficientes de calibración). STM32F103 a menudo simula EEPROM usando Flash.
  • Registros de Respaldo (BKP): En STM32, una pequeña área es alimentada por Vbat. Si la alimentación principal se desconecta pero hay una batería de botón, los datos aquí pueden conservarse.

Tabla Comparativa de Memorias

Tipo de Memoria Velocidad Pérdida al Apagar Contenido Almacenado Objeto de Operación
Flash Media No Código de programa, constantes const Compilador (automático)
SRAM Muy Rápida Variables, Heap/Stack, Buffers Código del desarrollador (lógica)
Registros Instantánea Operandos de instrucción, estado del hardware CPU o Drivers de Bajo Nivel
EEPROM Lenta No Parámetros del sistema, configuraciones de usuario Funciones de lectura/escritura específicas

DMA (Direct Memory Access, Acceso Directo a Memoria) es una tecnología de aceleración de hardware extremadamente importante. Su función principal es: transferir datos de forma totalmente automática y a alta velocidad entre la memoria (Flash, SRAM) y los periféricos (ADC, TIM, USART, etc.) sin consumir tiempo de CPU.

Puedes imaginar la CPU como el "CEO de la empresa", y DMA como el "transportista". Con DMA, el CEO no necesita mover personalmente cada byte de datos; solo necesita dar instrucciones sobre "desde dónde mover, a dónde mover, cuánto mover", y luego puede dedicarse a cálculos de mayor valor.

Código Básico de Configuración DMA

Ejemplo de ADC1 muestreando y transfiriendo datos vía DMA a un array en SRAM:


#include "stm32f10x.h"

// Buffer en SRAM para almacenar los resultados de ADC
uint16_t AD_Value[4];

void AD_Init(void)
{
   // Habilitar relojes para ADC1 (APB2), GPIOA (APB2), y DMA1 (AHB)
   RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
   RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
   RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

   // Configurar reloj del ADC (PCLK2 / 6 = 12MHz)
   RCC_ADCCLKConfig(RCC_PCLK2_Div6);

   // Configuración de pines GPIO A como entrada analógica (PA0-PA3)
   GPIO_InitTypeDef GPIO_InitStructure;
   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
   GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
   GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
   GPIO_Init(GPIOA, &GPIO_InitStructure);

   // Configurar canales regulares del ADC1
   // Canal 0, Secuencia 1, Tiempo Muestreo 55.5 ciclos
   ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
   // Canal 1, Secuencia 2, Tiempo Muestreo 55.5 ciclos
   ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
   // Canal 2, Secuencia 3, Tiempo Muestreo 55.5 ciclos
   ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);
   // Canal 3, Secuencia 4, Tiempo Muestreo 55.5 ciclos
   ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);

   // Configuración general del ADC
   ADC_InitTypeDef ADC_InitStructure;
   ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
   ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
   ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // Disparo por software
   ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;  // Modo de conversión continua: tras finalizar, reinicia automáticamente la siguiente.
   ADC_InitStructure.ADC_ScanConvMode = ENABLE;    // Modo de escaneo: Múltiples canales en el grupo regular, se convierten secuencialmente.
   ADC_InitStructure.ADC_NbrOfChannel = 4; // Número de canales regulares: 4 (ADC_Channel_0 a ADC_Channel_3)
   ADC_Init(ADC1, &ADC_InitStructure);

   // Configuración del DMA
   DMA_InitTypeDef DMA_InitStructure;
   // Dirección base del periférico: Registro de datos del ADC1 (ADC1->DR)
   DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
   // Ancho de datos del periférico: HalfWord (16 bits, ya que ADC_DR es de 16 bits)
   DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
   // Incremento de dirección del periférico: Deshabilitado (siempre se lee desde ADC_DR)
   DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
   // Dirección base de la memoria: Inicio del array AD_Value en SRAM
   DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;
   // Ancho de datos de la memoria: HalfWord (16 bits)
   DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
   // Incremento de dirección de la memoria: Habilitado (se escribe secuencialmente en AD_Value[0], AD_Value[1], ...)
   DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
   // Dirección de transferencia: Periférico a Memoria
   DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
   // Tamaño del buffer de transferencia: 4 (corresponde a los 4 resultados de conversión de canal)
   DMA_InitStructure.DMA_BufferSize = 4;
   // Modo de transferencia: Circular (tras completar la transferencia, se reinicia automáticamente)
   DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
   // Memoria a Memoria: Deshabilitado
   DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
   // Prioridad del canal DMA: Media
   DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
   // Inicializar el Canal 1 de DMA1
   DMA_Init(DMA1_Channel1, &DMA_InitStructure);

   // Habilitar el canal DMA 1
   DMA_Cmd(DMA1_Channel1, ENABLE);
   // Habilitar el comando DMA para el ADC1
   ADC_DMACmd(ADC1, ENABLE);
   // Habilitar el ADC1
   ADC_Cmd(ADC1, ENABLE);

   // Calibración del ADC
   ADC_ResetCalibration(ADC1);
   while (ADC_GetResetCalibrationStatus(ADC1) == SET);
   ADC_StartCalibration(ADC1);
   while (ADC_GetCalibrationStatus(ADC1) == SET);

   // Iniciar la conversión ADC por software
   ADC_SoftwareStartConvCmd(ADC1, ENABLE);

   // Ahora, el ADC convierte continuamente los datos de CH0-CH3. Cada vez que un canal
   // termina de convertirse, DMA automáticamente mueve el resultado al array AD_Value.
   // Después de mover 4 datos, reinicia desde el principio. La CPU no necesita intervenir en este proceso.
   // Para configurar conversión única y modo DMA no circular, se necesitaría reconfigurar el contador DMA,
   // deshabilitando DMA, recargando el contador y volviendo a habilitar DMA.
}
   

Explicación Detallada

(1). Mecanismo de Funcionamiento del DMA

El DMA está conectado al bus AHB. Cuando un periférico tiene datos listos (ej. ADC ha terminado de convertir), envía una "solicitud" al DMA. Al obtener permiso, el DMA toma instantáneamente el control del bus, completa una transferencia y luego lo libera. La CPU no es consciente de este proceso ni se bloquea.

(2). Tres Elementos Centrales (Los Tres Elementos de la Transferencia)

  • Dirección Origen y Destino: De dónde vienen los datos y adónde van.
  • Contador de Transferencia (BufferSize): Cuántos datos se deben mover en esta tarea. Cada vez que se mueve uno, el contador disminuye en 1. Al llegar a 0, se detiene (o entra en bucle).
  • Fuente de Disparo (Trigger Source): Quién llama para "Iniciar la transferencia". Puede ser una conversión ADC completada, un byte recibido por UART, o un evento de temporizador.

(3). Cuatro Direcciones de Transferencia Comunes

  • Periférico → Memoria: Muestras ADC guardadas en variables, datos recibidos por UART en un buffer.
  • Memoria → Periférico: Enviar una tabla de formas de onda predefinida (SRAM) a una salida DAC, enviar una cadena de texto por UART.
  • Memoria → Memoria: Copia de memoria pura, mucho más rápida que la función memcpy.
  • Periférico → Periférico: Poco común, pero utilizado en algunas configuraciones avanzadas.

(4). ¿Por Qué el Modo Escaneo Requiere DMA?

Como se mencionó anteriormente, el modo escaneo del grupo regular del ADC solo tiene un registro DR.

  • Sin DMA, el resultado de CH1 sobrescribirá instantáneamente el de CH0.
  • Con DMA, justo cuando CH0 termina de convertirse, el hardware, antes de que la CPU reaccione, mueve el valor de DR a Array[0]; luego, al terminar CH1, lo mueve a Array[1]. ¡Este es el método único y fiable para la adquisición de datos multicanal!

Resumen: Ventajas del DMA

Dimensión Sin DMA (Polling/Interrupción) Con DMA
Uso de CPU Muy Alto (interrupciones frecuentes por movimiento de datos) Muy Bajo (solo se maneja el resultado final después de la transferencia)
Tiempo Real de Datos Limitado por la velocidad de respuesta de interrupciones Respuesta a nivel de nanosegundos (controlado automáticamente por el hardware del bus)
Rendimiento (Throughput) Pequeño, adecuado para un solo byte Enorme, adecuado para datos continuos en grandes buffers

El tutorial de librería estándar para los periféricos básicos de STM32 ha concluido. Si hay alguna inexactitud, agradecería la corrección de los expertos. Planeo cubrir UART, I2C y otros en un tutorial posterior sobre protocolos de comunicación.

Etiquetas: STM32 Librería Estándar GPIO Interrupciones Temporizadores

Publicado el 6-14 19:52