29 de enero de 2016

ADC STM32F4 Discovery - Parte 2

Como ya comenté en la primera parte, la siguiente configuración permite solventar los fallos que presentaba la anterior además de conseguir una ejecución mas rápida al emplear el DMA (del ingles, Direct Memory Access). 
Así pues, lo primero que veremos serán las modificaciones necesarias en la función de configuración del ADC para comunicarse con el DMA.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void ADC_Configure (void)
{
    ADC_InitTypeDef ADC_InitStructure;
    ADC_CommonInitTypeDef ADC_CommonInitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB2PeriphClockCmd (RCC_APB2Periph_ADC1, ENABLE);

    ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
    ADC_InitStructure.ADC_ScanConvMode = ENABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
    ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConvEdge_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfConversion = ADCDataLength;
    ADC_Init (ADC1, &ADC_InitStructure);

    ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent;
    ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div2;
    ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled;
    ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles;
    ADC_CommonInit (&ADC_CommonInitStructure);

    ADC_TempSensorVrefintCmd (ENABLE);
    ADC_VBATCmd (ENABLE);

    ADC_RegularChannelConfig (ADC1, ADC_Channel_1, 1, ADC_SampleTime_3Cycles);
    ADC_RegularChannelConfig (ADC1, ADC_Channel_16, 2, ADC_SampleTime_3Cycles);
    ADC_RegularChannelConfig (ADC1, ADC_Channel_17, 3, ADC_SampleTime_3Cycles);
    ADC_RegularChannelConfig (ADC1, ADC_Channel_18, 4, ADC_SampleTime_3Cycles);

    ADC_ITConfig (ADC1, ADC_IT_EOC, ENABLE);

    ADC_DMARequestAfterLastTransferCmd (ADC1, ENABLE);
    ADC_DMACmd (ADC1, ENABLE);

    NVIC_InitStructure.NVIC_IRQChannel = ADC_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x0F;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0F;
    NVIC_Init (&NVIC_InitStructure);

    ADC_Cmd (ADC1, ENABLE);
}

Se ha quitado el trigger del ADC para hacerlo mediante la función ADC_SfotwareStartConv (ejecutada en la interrupción del botón de usuario) y se han añadido las funciones ADC_DMARequestAfterLastTransferCmd (permite al ADC realizar la petición de uso del DMA, ademas se encarga de limpiar el flag ADC_FLAG_EOC, por lo que no habrá que hacerlo dentro de la rutina de atención a la interrupción) y ADC_DMACmd (habilita la comunicación entre el ADC y el DMA).
Ahora toca configurar el canal del DMA para que se comunique con el ADC. Para ello comenzaremos dando unas menciones del DMA viendo su diagrama de bloques de funcionamiento.


En la siguiente imagen se muestran otros detalles relacionados con el diagrama anterior.


El DMA permite a ciertos periféricos acceder directamente a memoria para leer y/o escribir datos sin intermediar con el procesador, de forma que se libera a este de la carga de transferencia de datos y le permite seguir atendiendo a otras tareas. Esto permite una mejor comunicación entre la memoria y los periféricos de baja velocidad que de otra forma generarían una elevada carga de interrupciones al procesador. Todo esto no quita que se necesite el uso del bus del sistema (AHB en este caso), de forma que se existen distintas estrategias que consiguen que el DMA no acapare por completo el uso de este. De entre estas estrategias o protocolos interesa conocer estos tres:
  • Burst mode (modo ráfaga): el DMA toma el control del bus del sistema y no lo libera hasta que completa la transferencia. Es el modo mas rápido de transferir los datos pero por el contrario el que mas efecto negativo produce sobre el procesador al acaparar por completo el control del bus. Este modo se puede usar con mayor eficacia en procesadores que cuenten con memoria cache para que este pueda seguir trabajando.
  • Cycle stealing mode (modo robo de ciclo): el DMA genera una petición de control del bus al procesador y cuando este se lo asigna, envía un byte de datos y devuelve el control al procesador. Este juego continua hasta que se completa la transferencia. De esta forma se permite al procesador continuar con sus tareas (ademas también de atender las peticiones de control del bus), pero por contra, la transferencia de datos resulta mas lenta.
  • Transparent mode  (modo transparente): el DMA hace uso del bus del sistema cuando este se encuentra disponible, de forma que no interrumpe en ningún momento al procesador. Este modo es el mejor en cuanto a términos de rendimiento del sistema, pero por el contrario necesita determinar cuando el bus se encuentra libre, lo cual resulta complejo
Para mas información, visitar Direct memory access.
Tras esto, vemos la configuración del DMA y se podrán relacionar ciertos parámetros con los diagramas anteriores.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void DMA_Configure (void)
{
    DMA_InitTypeDef DMA_InitStructure;

    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);

    DMA_InitStructure.DMA_Channel = DMA_Channel_0;
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t) &ADC1->DR;
    DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t) ADCData;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
    DMA_InitStructure.DMA_BufferSize = ADCDataLength;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
    DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_1QuarterFull;
    DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
    DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
    DMA_Init (DMA2_Stream0, &DMA_InitStructure);

    DMA_Cmd (DMA2_Stream0, ENABLE);
}

Lo primero, como siempre, será activare el reloj para el periférico. Tras esto, escogemos el canal por el cual serán recogidos los datos. Para verificar esto nos remitiremos a la tabla 43 del archivo RM0090 Reference Manual, en la cual se puede ver que es el DMA2 el que permite la petición del ADC1.


Especificaremos la dirección del registro de donde se obtendrán los datos y la dirección donde serán almacenados. Indicamos la sentido del flujo de datos, desde el ADC a la memoria y la cantidad de datos a transmitir. El incremento de dirección en el periférico se desactiva (leemos siempre del mismo) pero no la de la memoria, puesto que hay mas de un dato que guardar. El tamaño de los datos a transmitir es de 16 bits y se activará el modo circular, de forma que cuando se reciben todos los datos, el puntero de dirección se reinicia en el valor configurado y guarda de nuevo los datos en la misma ubicación (para no perder los anteriores se tendrán que extraer durante la interrupción de fin de conversión del ADC). Establecemos la prioridad para la transferencia del ADC y se desactiva el modo FIFO. La transferencia de datos sera en ráfaga simple. Finalmente escogemos el stream correspondiente de la tabla ya comentada e inicializamos el modulo DMA.
Como se comentó mas arriba, dado que la función ADC_DMARequestAfterLastTransferCmd limpia el flag de la interrupcion de fin de conversión, en dicha interrupción no quedaría mas que hacer un backup de los datos actuales para no perderlos con la siguiente conversión.
En el main se mantiene el funcionamiento inicial, pero hay que modificar el origen de los datos convertidos, siendo este ADCData.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
int main (void)
{
    float vTempSensor = 0, temp = 0, Vbat = 0;

    GPIO_Configure ();
    ADC_Configure ();
    EXTI_Configure ();
    DMA_Configure ();

    while (1)
    {
        if (ADCData [0] == 0)
        {
            GPIO_ResetBits (GPIOD, GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15);
        }
        else if (ADCData [0] > 0 && ADCData [0] < 1024)
        {
            GPIO_SetBits (GPIOD, GPIO_Pin_13);
            GPIO_ResetBits (GPIOD, GPIO_Pin_12 | GPIO_Pin_14 | GPIO_Pin_15);
        }
            else if (ADCData [0] >= 1024 && ADCData [0] < 2048)
        {
            GPIO_SetBits (GPIOD, GPIO_Pin_13 | GPIO_Pin_14);
            GPIO_ResetBits (GPIOD, GPIO_Pin_12 | GPIO_Pin_15);
        }
        else if (ADCData [0] >= 2048 && ADCData [0] < 3072)
        {
            GPIO_SetBits (GPIOD, GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15);
            GPIO_ResetBits (GPIOD, GPIO_Pin_12);
        }
        else if (ADCData [0] >= 3072 && ADCData [0] < 4096)
        {
            GPIO_SetBits (GPIOD, GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15);
        }

        vTempSensor = (float) ADCData [1] / 4095;    // vTempSensor (mV) = 3300 * (TempSensor_ConvertedValue / 4095)
        vTempSensor *= 3300;
        temp = vTempSensor - 760;                // temp (ºC) = ((Vsense - V25) / Average_slope) + 25
        temp /= 2.5;
        temp += 25;

        Vbat = (float) ADCData [3] * 2;    // Internal bridge divider by 2
        Vbat /= 4095;                    // Vbat (mV) = 3300 * ((Vbat_ConvertedValue * 2) / 4095)
        Vbat *= 3300;
    }
}

Una sencilla e interesante modificación seria que tras la interrupción del botón de usuario y activación de la conversión, esta continuara automáticamente, para lo cual simplemente habría que habilitar el modo continuo del ADC.
Hasta aquí el sustito al último ejemplo de la primera parte que no funcionó correctamente y el proyecto listo en el siguiente enlace: ADC DMA STM32F4 Discovery Dropbox

Dado que todo este tiempo hemos trabajado con un solo ADC, a continuación vemos como podemos usar los tres ADCs junto con el DMA.
Inicialmente escogeremos los canales que queremos convertir, en definitiva, configurar los pines de entrada correspondientes. En mi caso escogi desde el pin PA1 hasta el PA6, que se corresponden con los canales 1 a 6: los canales 1 y 2 se usaran con el ADC3, los canales 3 y 4 con el ADC2 y los canales 5 y 6 con el ADC1, como se ve a continuación en la configuración del ADC.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void ADC_Configure (void)
{
    ADC_InitTypeDef ADC_InitStructure;
    ADC_CommonInitTypeDef ADC_CommonInitStructure;

    RCC_APB2PeriphClockCmd (RCC_APB2Periph_ADC1, ENABLE);
    RCC_APB2PeriphClockCmd (RCC_APB2Periph_ADC2, ENABLE);
    RCC_APB2PeriphClockCmd (RCC_APB2Periph_ADC3, ENABLE);

    ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
    ADC_InitStructure.ADC_ScanConvMode = ENABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
    ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConvEdge_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfConversion = 2;
    ADC_Init (ADC1, &ADC_InitStructure);
    ADC_Init (ADC2, &ADC_InitStructure);
    ADC_Init (ADC3, &ADC_InitStructure);

    ADC_CommonInitStructure.ADC_Mode = ADC_TripleMode_RegSimult;
    ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div2;
    ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_1;
    ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles;
    ADC_CommonInit (&ADC_CommonInitStructure);

    ADC_RegularChannelConfig (ADC3, ADC_Channel_1, 1, ADC_SampleTime_3Cycles);
    ADC_RegularChannelConfig (ADC3, ADC_Channel_2, 2, ADC_SampleTime_3Cycles);
    ADC_RegularChannelConfig (ADC2, ADC_Channel_3, 1, ADC_SampleTime_3Cycles);
    ADC_RegularChannelConfig (ADC2, ADC_Channel_4, 2, ADC_SampleTime_3Cycles);
    ADC_RegularChannelConfig (ADC1, ADC_Channel_5, 1, ADC_SampleTime_3Cycles);
    ADC_RegularChannelConfig (ADC1, ADC_Channel_6, 2, ADC_SampleTime_3Cycles);

    ADC_MultiModeDMARequestAfterLastTransferCmd (ENABLE);
    ADC_DMACmd (ADC1, ENABLE);

    ADC_Cmd (ADC1, ENABLE);
    ADC_Cmd (ADC2, ENABLE);
    ADC_Cmd (ADC3, ENABLE);
}

Lo primero será activar los relojes de los ADC2 y ADC3 y a continuación estableceremos la misma configuración de funcionamiento para los tres. En la estructura de configuración común es donde se introducen los cambios mas importantes. El primero de ellos es la elección del modo triple simultaneo para canales regulares, que junto con la elección del modo de acceso 1 del DMA determinan el siguiente funcionamiento:



El flujo de las conversiones va desde el ADC1 al ADC3. En la imagen anterior se obtendrían los datos en el siguiente orden: ADC1-CH0, ADC2-CH15, ADC3-CH10, ADC1-CH1... Finalmente activamos la petición del DMA y lo inicializamos desde el ADC1, puesto que este actúa como master cuando trabajamos en modo multiADC.



Una vez configurado correctamente los ADCs, pasamos al DMA. 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void DMA_Configure (void)
{
    DMA_InitTypeDef DMA_InitStructure;

    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);

    DMA_InitStructure.DMA_Channel = DMA_Channel_0;
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t) ADC_CDR_ADDRESS;
    DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t) ADCData;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
    DMA_InitStructure.DMA_BufferSize = ADCDataLength;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
    DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_1QuarterFull;
    DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
    DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
    DMA_Init (DMA2_Stream0, &DMA_InitStructure);

    DMA_Cmd (DMA2_Stream0, ENABLE);
}

El aspecto mas importante aquí es el registro de donde se leerán en este caso los datos, ADC_CDR, siendo el valor de su dirección

1
#define ADC_CDR_ADDRESS ((uint32_t)0x40012308)

Como se ha podido ver, en este caso no se necesita de ninguna interrupción, salvo la del botón de usuario para iniciar la conversión.
Finalmente, en el main se hará otra decodificación simple para verificar que los datos que lee el ADC corresponden con los que esperamos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int main (void)
{
    GPIO_Configure ();
    ADC_Configure ();
    EXTI_Configure ();
    DMA_Configure ();

    while (1)
    {
        if (ADCData [0] < 4000)
            GPIO_ResetBits (GPIOD, GPIO_Pin_13);
        else
            GPIO_SetBits (GPIOD, GPIO_Pin_13);

        if (ADCData [1] < 4000)
            GPIO_ResetBits (GPIOD, GPIO_Pin_14);
        else
            GPIO_SetBits (GPIOD, GPIO_Pin_14);

        if (ADCData [2] < 4000)
            GPIO_ResetBits (GPIOD, GPIO_Pin_15);
        else
            GPIO_SetBits (GPIOD, GPIO_Pin_15);

        if (ADCData [3] < 4000)
            GPIO_ResetBits (GPIOD, GPIO_Pin_12);
        else
            GPIO_SetBits (GPIOD, GPIO_Pin_12);
    }
}

Se compara el valor leído para determinar si los canales 1 a 4 (no hay leds para los otros dos canales) están a nivel alto. En el siguiente enlace se encuentra disponible la configuración expuesta: ADC Triple Mode STM32F4 Discovery Dropbox.

Por ultimo, para terminar con el ADC, veremos como usar el analog watchdog disponible. Este "vigila" si el valor convertido esta fuera del rango establecido por dos umbrales programables. Para demostrar esto configuraremos el ADC1 para que convierta continuamente un canal y estableceremos los umbrales en 0.5 y 2.5 V, de forma que si estamos fuera del rango se encenderá un led.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void ADC_Configure (void)
{
    ADC_InitTypeDef ADC_InitStructure;
    ADC_CommonInitTypeDef ADC_CommonInitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB2PeriphClockCmd (RCC_APB2Periph_ADC1, ENABLE);

    ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
    ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConvEdge_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfConversion = 1;
    ADC_Init (ADC1, &ADC_InitStructure);

    ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent;
    ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div2;
    ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled;
    ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles;
    ADC_CommonInit (&ADC_CommonInitStructure);

    ADC_RegularChannelConfig (ADC1, ADC_Channel_1, 1, ADC_SampleTime_3Cycles);

    ADC_ITConfig (ADC1, ADC_IT_AWD, ENABLE);

    NVIC_InitStructure.NVIC_IRQChannel = ADC_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x2;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x2;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init (&NVIC_InitStructure);

    ADC_AnalogWatchdogSingleChannelConfig (ADC1, ADC_Channel_1);
    ADC_AnalogWatchdogThresholdsConfig (ADC1, (uint16_t) ((2500 * 4095) / 3300), (uint16_t) ((500 * 4095) / 3300));
    ADC_AnalogWatchdogCmd (ADC1, ADC_AnalogWatchdog_SingleRegEnable);

    ADC_Cmd (ADC1, ENABLE);
}

Las funciones de configuración del analog watchdog son muy sencillas, por lo que no requieren comentarios adicionales.
Y la rutina de atención a la interrupción donde se enciende el led.

1
2
3
4
5
6
7
8
void ADC_IRQHandler (void)
{
    if (ADC_GetITStatus (ADC1, ADC_IT_AWD) != RESET)
    {
        GPIO_SetBits (GPIOD, GPIO_Pin_13);
        ADC_ClearITPendingBit (ADC1, ADC_IT_AWD);
    }
}

Este ejemplo resultaría interesante para crear una alarma a partir del canal del sensor de temperatura. También dejo a continuación el archivo con la configuración del analog watchdog vista: ADC Analog Watchdog STM32F4 Discovery Dropbox.

24 de enero de 2016

ADC STM32F4 Discovery - Parte 1

Anteriormente veíamos lo importante que resultaban los temporizadores en los sistemas electrónicos actuales. Al igual que estos, existen otros periféricos, que debido a su interacción con el medio, también resultan de gran interés para el desarrollo de aplicaciones.
Por ello, el siguiente periférico con el que trabajaremos será el convertidor analógico a digital (ADC, del inglés, Analog to Digital Converter), que se encarga de convertir una señal en tensión de naturaleza analógica a un valor digital con el que es posible realizar el procesamiento de dicha señal.

Antes de mostrar ejemplos sencillos de las distintas configuraciones del ADC de nuestra placa, veremos una pequeña introducción (mas información en Analog to digital converter, Wikipedia) a los convertidores analógicos a digital para tratar unos aspectos interesantes.
Cuando realizamos la conversión de analógico a digital, debemos tener en cuenta dos aspectos principales, el muestreo y la cuantificación y codificación de dicha señal, lo cual, en simples palabras, significa que la conversión de analógico a digital produce una discretización de la señal en tiempo y amplitud.
El muestreo consiste en tomar una muestra (un valor) de la señal monitorizada en un instante de tiempo. Dado que el sistema electrónico que se encargue de esto no tiene la capacidad de capturar los infinitos valores que toma la señal en el tiempo, estaremos discretizando la señal en el tiempo. De aquí se deriva el término frecuencia de muestreo, que indica el intervalo de tiempo entre cada muestra de la señal analógica. Entrando aun mas en teoría, conviene saber que para muestrear "correctamente" una señal analógica, la frecuencia escogida ha de ser mayor que el doble de la frecuencia máxima de dicha señal. Dicho valor es conocido como frecuencia de Nyquist (Teorema de Nyquist, Wikipedia).
Una vez obtenidas las muestras necesarias de la señal, se procede a cuantificar y codificar dichos valores. Aquí entra en juego la resolución, en bits, del conversor que se este utilizando. En nuestro caso, como se verá a continuación, disponemos de un ADC de 12 bits de resolución, lo que nos permite codificar una medida desde 0 hasta 4095 (cuantificación de 4096 valores codificados desde 0 a 4095). Aquí nuevamente volvemos a ver el efecto de la discretización, en este caso en amplitud, y al igual que la discretización en el tiempo, es muy importante, ya que una elección incorrecta de la resolución del convertidor derivará en una reducción de la relación señal ruido (SNR, del inglés, Signal Noise Ratio).

Visto esto, pasamos a ver detalles específicos del ADC (en realidad dispone de 3, según podemos leer en su datasheet) disponible en la placa STM32F4 Discovery.
Sus características mas interesantes son (ver RM0090 Reference Manual):
  • Resolución configurable: 6, 8, 10 y 12 bits.
  • 19 canales multiplexados: 16 de ellos conectados a los puertos de entrada/salida, 2 canales internos conectados a un sensor de temperatura y para medir la tensión de referencia del ADC y un tercero para el canal de la batería para RTC (del inglés, Real Time Clock).
  • Configuración de lectura: conversión única o continua sobre uno o varios canales y modos scan para conversión automática desde el canal 0 al canal 'n' o discontinuo.
  • Alineación a izquierda o derecha en registro de 16 bits del valor convertido.
  • Almacenamiento a través de DMA (del inglés, Direct Memory Access).
A continuación, y como es de costumbre, vemos el diagrama de bloques del microcontrolador para identificar el bus de conexión del ADC.


Como se puede apreciar, se encuentra conectado al APB2 y también podemos identificar los 3 ADC a los que se hacia referencia mas arriba.

Y en la siguiente imagen podemos ver el diagrama de bloques del ADC.


Tras esto veremos una tabla resumen de los canales del ADC para conocer su asignación a cada pin de entrada/salida de la placa y conexiones internas.

ADC Channel
I/O Pin
Internal
ADC123_IN0
PA0
-
ADC123_IN1
PA1
-
ADC123_IN2
PA2
-
ADC123_IN3
PA3
-
ADC12_IN4
PA4
-
ADC12_IN5
PA5
-
ADC12_IN6
PA6
-
ADC12_IN7
PA7
-
ADC12_IN8
PB0
-
ADC12_IN9
PB1
-
ADC123_IN10
PC10
-
ADC123_IN11
PC11
-
ADC123_IN12
PC12
-
ADC123_IN13
PC13
-
ADC12_IN14
PC14
-
ADC12_IN15
PC15
-
ADC1_IN16
-
Temperature sensor
ADC1_IN17
-
Vref
ADC1_IN18
-
Vbat

Como se aprecia, no todos los canales de los ADC disponibles están asignados, en concreto algunos del ADC3. Para seleccionar un puerto de entrada/salida antes debemos cerciorarnos de que este no esta siendo usado por otro periférico, lo cual podemos comprobar en UM1472 User Manual.
Visto esto, pasaremos a realizar algunos ejemplos de programación del ADC.

Inicialmente configuraremos un solo canal y realizaremos la lectura, primero por software, y a continuación mediante trigger (pin de entrada/salida o timer).
Lo primero que haremos, como siempre, será configurar el pin de entrada/salida. En este caso elegiremos el pin PA1, pues el PA0 esta conectado al botón de usuario. También haremos lo mismo con los pines de los leds para generar una visualización que varie con el valor medido.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void GPIO_Configure (void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_AHB1PeriphClockCmd (RCC_AHB1Periph_GPIOA, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;
//    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
//    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_Init (GPIOA, &GPIO_InitStructure);

    /* Leds */
    RCC_AHB1PeriphClockCmd (RCC_AHB1Periph_GPIOD, ENABLE);

                                /* LED4 Green | LED3 Orange |  LED5 Red   | LED6  Blue */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_Init (GPIOD, &GPIO_InitStructure);
}

La configuración del ADC es algo mas larga pero igualmente sencilla. A continuación la vemos y se detallan algunos parámetros.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void ADC_Configure (void)
{
    ADC_InitTypeDef ADC_InitStructure;
    ADC_CommonInitTypeDef ADC_CommonInitStructure;

    RCC_APB2PeriphClockCmd (RCC_APB2Periph_ADC1, ENABLE);

    ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
    ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConvEdge_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfConversion = 1;
    ADC_Init (ADC1, &ADC_InitStructure);

    ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent;
    ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div2;
    ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled;
    ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles;
    ADC_CommonInit (&ADC_CommonInitStructure);

    ADC_RegularChannelConfig (ADC1, ADC_Channel_1, 1, ADC_SampleTime_3Cycles);

    ADC_Cmd (ADC1, ENABLE);
}

Seleccionamos la máxima resolución del ADC, puesto que no tenemos ningún tipo de restricción temporal que nos obligue a reducir esta y desactivamos los modos scan y continuo, ya que vamos a usar un único canal (ADC_ScanConvMode = DISABLE) y el bucle principal será quien haga la petición de conversión (ADC_ContinuousConvMode = DISABLE). Como vamos a realizar la conversion por software, no estableceremos el flanco del trgigger (ADC_ExternalTrigConvEdge = AD_ExternalTrigConvEdge_None), de forma que el siguiente valor no tiene efecto. Los datos estarán justificados a la derecha (el bit menos significativo del valor obtenido coincide con el bit menos significativo del registro que lo contenga). Para que el ADC pueda realizar una única conversión deberá estar en modo independiente. Con intención de obtener una alta tasa de muestreo, se ha divido el reloj del APB2 por 2 para que el reloj del ADC sea de 42 MHz. Se ha desactivado el modo DMA e igualmente se ha escogido el valor mas pequeño para el intervalo de tiempo entre conversiones del ADC.
El cálculo de la tasa de muestreo del ADC en este caso es muy sencilla. Tan solo se ha de conocer el numero de ciclos de reloj que requiere la conversión con una resolución de 12 bits, siendo este igual a 12. A esto hay que añadirle ADC_SampleTime_3Cycles de la configuración del canal y la mitad de los ciclos entre conversiones, es decir, 2.5 ciclos (no tiene sentido hablar de medio ciclo). Con esto nos queda lo siguiente:

$$\frac { 12+3+2.5 }{ 42 } =0.416667us\rightarrow \frac { 1 }{ 0.416667 } =2.4Msps$$

Se obtiene una tasa de muestreo de 2.4 Msps, que coincide con el valor dado en el datasheet de este.

Por último, la función que se encarga de realizar la conversión es la siguiente.

1
2
3
4
5
6
uint16_t ADC_Read (void)
{
    ADC_SoftwareStartConv (ADC1);
    while (ADC_GetFlagStatus (ADC1, ADC_FLAG_EOC) == RESET);
    return ADC_GetConversionValue (ADC1);
}

Simplemente llama a la función de librería ADC_SoftwareStartConv y espera a que termine la conversión para devolver el valor obtenido, en este caso a una variable local del main como se ve a continuación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int main (void)
{
    uint16_t ADC_ConvertedValue = 0;

    GPIO_Configure ();
    ADC_Configure ();

    while (1)
    {
        ADC_ConvertedValue = ADC_Read ();

        if (ADC_ConvertedValue == 0)
        {
            GPIO_ResetBits (GPIOD, GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15);
        }
        else if (ADC_ConvertedValue > 0 && ADC_ConvertedValue < 1024)
        {
            GPIO_SetBits (GPIOD, GPIO_Pin_13);
            GPIO_ResetBits (GPIOD, GPIO_Pin_12 | GPIO_Pin_14 | GPIO_Pin_15);
        }
        else if (ADC_ConvertedValue >= 1024 && ADC_ConvertedValue < 2048)
        {
            GPIO_Pin_13 | GPIO_Pin_14);
            GPIO_ResetBits (GPIOD, GPIO_Pin_12 | GPIO_Pin_15);
        }
        else if (ADC_ConvertedValue >= 2048 && ADC_ConvertedValue < 3072)
        {
            GPIO_SetBits (GPIOD, GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15);
            GPIO_ResetBits (GPIOD, GPIO_Pin_12);
        }
        else if (ADC_ConvertedValue >= 3072 && ADC_ConvertedValue < 4096)
        {
            GPIO_SetBits (GPIOD, GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15);
        }
    }
}

Aquí, a parte de la lectura del valor convertido, se hace una pequeña decodificación, de forma que mientras mayor sea el valor leído, mas leds se encenderán. Si intentamos sacar mayor provecho a esta programación, podemos hacer que la función de lectura ADC_Read () tenga un parámetro que sea el canal a leer. Asi pues, si además del canal ya configurado, queremos también poder leer los canales internos (sensor de temperatura, tensión de referencia y monitorización de batería), procederíamos de la siguiente forma. En ADC_Config () añadiríamos la activación de dichos canales con las siguientes lineas

1
2
ADC_TempSensorVrefintCmd (ENABLE);
ADC_VBATCmd (ENABLE);

y eliminaríamos la linea de configuración del canal regular, que pasaría a la función de lectura de la siguiente forma:

1
2
3
4
5
6
7
uint16_t ADC_Read (uint8_t ADC_Channel)
{
    ADC_RegularChannelConfig (ADC1, ADC_Channel, 1, ADC_SampleTime_3Cycles);
    ADC_SoftwareStartConv (ADC1);
    while (ADC_GetFlagStatus (ADC1, ADC_FLAG_EOC) == RESET);
    return ADC_GetConversionValue (ADC1);
}

Para leer los valores modificaremos de la siguiente nuestro main:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
int main (void)
{
    uint16_t ADC_ConvertedValue = 0, TempSensor_ConvertedValue = 0, Vref_ConvertedValue = 0, Vbat_ConvertedValue = 0;
    float vTempSensor = 0, temp = 0, Vbat = 0;

    GPIO_Configure ();
    ADC_Configure ();

    while (1)
    {
        ADC_ConvertedValue = ADC_Read (ADC_Channel_1);

        if (ADC_ConvertedValue == 0)
        {
            GPIO_ResetBits (GPIOD, GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15);
        }
        else if (ADC_ConvertedValue > 0 && ADC_ConvertedValue < 1024)
        {
            GPIO_SetBits (GPIOD, GPIO_Pin_13);
            GPIO_ResetBits (GPIOD, GPIO_Pin_12 | GPIO_Pin_14 | GPIO_Pin_15);
        }
        else if (ADC_ConvertedValue >= 1024 && ADC_ConvertedValue < 2048)
        {
            GPIO_SetBits (GPIOD, GPIO_Pin_13 | GPIO_Pin_14);
            GPIO_ResetBits (GPIOD, GPIO_Pin_12 | GPIO_Pin_15);
        }
        else if (ADC_ConvertedValue >= 2048 && ADC_ConvertedValue < 3072)
        {
            GPIO_SetBits (GPIOD, GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15);
            GPIO_ResetBits (GPIOD, GPIO_Pin_12);
        }
        else if (ADC_ConvertedValue >= 3072 && ADC_ConvertedValue < 4096)
        {
            GPIO_SetBits (GPIOD, GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15);
        }

        TempSensor_ConvertedValue = ADC_Read (ADC_Channel_16);
        vTempSensor = (float) TempSensor_ConvertedValue / 4095;    // vTempSensor (mV) = 3300 * (TempSensor_ConvertedValue / 4095)
        vTempSensor *= 3300;
        temp = vTempSensor - 760;    // temp (ºC) = ((Vsense - V25) / Average_slope) + 25
        temp /= 2.5;
        temp += 25;

        Vref_ConvertedValue = ADC_Read (ADC_Channel_17);

        Vbat_ConvertedValue = ADC_Read (ADC_Channel_18);
        Vbat = (float) Vbat_ConvertedValue * 2;    // Internal bridge divider by 2
        Vbat /= 4095;    // Vbat (mV) = 3300 * ((Vbat_ConvertedValue * 2) / 4095)
        Vbat *= 3300;
    }
}

La información de las expresiones mostradas se puede ver en UM1472 User Manual.
En el siguiente vídeo se puede ver una demostración de esto.


En el siguiente enlace a dropbox se encuentra la programación de este ultimo ejemplo: ADC SoftConv STM32F4 Discovery Dropbox

Para finalizar esta primera parte de la entrada, veremos como activar la conversión del ADC mediante un trigger (pin de entrada/salida o timer). En este caso nos decantaremos por la opción de la interrupción externa, de modo que procederemos a configurar el puerto PD11 (podría ser cualquier otro puerto pero siempre el pin 11) como entrada digital y seguidamente su interrupción, como se ve a continuación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
void EXTI_Configure (void)
{
    EXTI_InitTypeDef EXTI_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB2PeriphClockCmd (RCC_APB2Periph_SYSCFG, ENABLE);

    SYSCFG_EXTILineConfig (EXTI_PortSourceGPIOD, EXTI_PinSource11);

    EXTI_InitStructure.EXTI_Line = EXTI_Line11;
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
    EXTI_Init (&EXTI_InitStructure);

    NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x2;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x2;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init (&NVIC_InitStructure);
}

En mi caso puenteé el pin PA0 conectado al botón de usuario con el usado aquí para facilitar la generación del pulso para la interrupción.
También tendremos que modificar la configuración del ADC para que se active la conversión a partir de la interrupción programada.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void ADC_Configure (void)
{
    ADC_InitTypeDef ADC_InitStructure;
    ADC_CommonInitTypeDef ADC_CommonInitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB2PeriphClockCmd (RCC_APB2Periph_ADC1, ENABLE);

    ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
    ADC_InitStructure.ADC_ScanConvMode = ENABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
    ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_Rising;
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_Ext_IT11;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfConversion = 4;
    ADC_Init (ADC1, &ADC_InitStructure);

    ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent;
    ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div2;
    ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled;
    ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles;
    ADC_CommonInit (&ADC_CommonInitStructure);

    ADC_TempSensorVrefintCmd (ENABLE);
    ADC_VBATCmd (ENABLE);

    ADC_RegularChannelConfig (ADC1, ADC_Channel_1, 1, ADC_SampleTime_3Cycles);
    ADC_RegularChannelConfig (ADC1, ADC_Channel_16, 2, ADC_SampleTime_3Cycles);
    ADC_RegularChannelConfig (ADC1, ADC_Channel_17, 3, ADC_SampleTime_3Cycles);
    ADC_RegularChannelConfig (ADC1, ADC_Channel_18, 4, ADC_SampleTime_3Cycles);

    ADC_EOCOnEachRegularChannelCmd (ADC1, ENABLE);

    ADC_ITConfig (ADC1, ADC_IT_OVR, ENABLE);
    ADC_ITConfig (ADC1, ADC_IT_EOC, ENABLE);

    NVIC_InitStructure.NVIC_IRQChannel = ADC_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x0F;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0F;
    NVIC_Init (&NVIC_InitStructure);

    ADC_Cmd (ADC1, ENABLE);
}

El funcionamiento esperado de este código es que tras la interrupción generada en el pin PD11 se active la conversión de los canales del ADC, de forma que con cada conversión, se genera una interrupción de fin de conversión (función ADC_EOCOnEachRegularChannelCmd), y tener un contador en la interrupción que controle cual es el canal convertido.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void ADC_IRQHandler (void)
{
    if (ADC_GetITStatus (ADC1, ADC_IT_EOC) != RESET)
    {
        static int i = 0;

        if (i == 0)
        {
            ADC_ConvertedValue = ADC_GetConversionValue (ADC1);
            i++;
        }
        else if (i == 1)
        {
            TempSensor_ConvertedValue = ADC_GetConversionValue (ADC1);
            i++;
        }
        else if (i == 2)
        {
            Vref_ConvertedValue = ADC_GetConversionValue (ADC1);
            i++;
        }
        else if (i == 3)
        {
            Vbat_ConvertedValue = ADC_GetConversionValue (ADC1);
            i = 0;
        }

        ADC_ClearITPendingBit (ADC1, ADC_IT_EOC);
    }
    else if (ADC_GetITStatus (ADC1, ADC_IT_OVR) != RESET)
    {
        ADC_ClearITPendingBit (ADC1, ADC_IT_OVR);
    }
}

void EXTI15_10_IRQHandler (void)
{
    if (EXTI_GetITStatus (EXTI_Line11) != RESET)
    {
        EXTI_ClearITPendingBit (EXTI_Line11);
    }
}

Ademas, para que el ADC sepa que tiene que realizar mas de una conversión, además de modificar el valor de ADC_NbrOfConversion se deberá activar el modo Scan
Como he dicho en el parrafo anterior, el funcionamiento esperado es el comentado. Pero al momento de probarlo no es así. El fallo (no he conseguido determinarlo) es que al inicio de la conversión, tras la detección de la interrupción, se activa el flag de Overrun (pérdida de datos de las conversiones), de forma que impide que la conversión de canales continúe. He intentado solventarlo, activando dicha interrupción y limpiando su flag, pero con la siguiente interrupción externa se vuelve a generar la comentada y no permite finalizar todas las conversiones. Si consigo dar con la solución o alguien tuviera idea de como resolverlo, bienvenida sea la ayuda para poderlo corregir.
He comentado todo esto debido a que, como expondré en la segunda parte, esta forma de convertir varios canales se prefiere usarla junto con el DMA, de forma que no se interrumpe al microcontrolador y se realiza mas rápido la transferencia de los valores convertidos a memoria. En cualquier caso, vuelvo a dejar un archivo de dropbox con esta configuración: ADC ExtTrig STM32F4 Discovery Dropbox

4 de enero de 2016

TIMER STM32F4 Discovery

Los sistemas electrónicos digitales destacan siempre por la capacidad para trabajar con tiempos con gran precisión. Esta funcionalidad recae sobre los Timers, periféricos imprescindible en todo microcontrolador, que partiendo de su funcionalidad genérica, permite desarrollar otras como la generación de un interrupciones, control PWM, input capture (permite determinar la frecuencia y ancho de pulso de una señal de entrada) o output compare (principio del PWM, generación de formas de ondas).
En esta ocasión pasaremos a ver unos ejemplos de la generación de una interrupción y el control de PWM, ya que no requieren de hardware externo para la comprobación de su funcionamiento. Mas adelante intentaré incluir algunos ejemplos para poder ver sus otras aplicaciones.

En primer lugar vamos a ver una introducción a la arquitectura del microcontrolador STM32F407 con respecto a los Timers. Como siempre partimos del diagrama de bloques en el que se aprecian todos los periféricos de los que disponemos.


Así pues, nuestro microcontrolador dispone de 14 timers, los cuales clasificaremos en tres grupos según su capacidad funcional (mas información de esto en RM0090 - Reference manual):
  • Timers de control avanzado (TIM1 y TIM8): consisten en un contador de 16 bits con autorecarga controlado por un prescaler programable. Permiten realizar todas las funcionalidades que se han comentado al inicio además de otras también relacionadas con el control PWM.

  • Timers de propósito general (TIM2 a TIM5 y TIM9 a TIM14): consisten en un contador de 16 o 32 bits con autorecarga controlado por un prescaler programable. Realizan las mismas funcionalidades que las comentadas al inicio pero con limitaciones en otras de las que si disponen los timers anteriores.

  • Timers básicos (TIM6 y TIM7): consisten en un contador de 16 bits con autorecarga controlado por un prescaler programable. Ademas de la generación básica de tiempos, permiten interactuar con otros periféricos (al igual que los anteriores), para el control de estos (como ADC y DAC).

En las imágenes presentadas se puede ver claramente como la complejidad del diagrama de bloques disminuye de una clasificación a la siguiente, y por descontado, la funcionalidad de los timers.

Un aspecto en común a todos los timers es el cálculo del valor que se cargará en el registro del contador (registro de autorecarga). Este se obtiene de forma muy sencilla a partir de la siguiente expresión:

$$CK_{ cnt }=\frac { { CK }_{ psc } }{ { TIMx }_{ psc }+1 }$$

Si despejamos el tiempo a cargar en el prescaler nos queda:

$${ TIMx }_{ psc }=\frac { { CK }_{ psc } }{ CK_{ cnt } } -1$$

Ahora tenemos el contador programado para una frecuencia específica. A partir de dicha frecuencia es sencillo calcular el valor que se cargará en el registro del este, pues simplemente se habrá de buscar la relación entre el periodo que queremos para la interrupción del timer y el periodo de la frecuencia seleccionada.

Visto esto, pasamos a exponer un ejemplo de configuración de uno de estos timers, en concreto el TIM7 (dentro de los timers básicos), para generar una interrupción con la que controlaremos la secuencia de encendido de los leds disponibles en la placa.

Empezaremos por configurar los puertos de entrada/salida para comandar los leds de la placa. Esto podemos verlo en entradas anteriores, GPIO STM32F4 Discovery.
Tras esto procedemos a la configuración del TIM7 y de su interrupción.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void TIMER_Configure (void)
{
    TIM_TimeBaseInitTypeDef TIMER_TimeBaseInitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB1PeriphClockCmd (RCC_APB1Periph_TIM7, ENABLE);

    TIMER_TimeBaseInitStructure.TIM_Prescaler = 8400 - 1;
    TIMER_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIMER_TimeBaseInitStructure.TIM_Period = 10000 - 1;
    TIMER_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIMER_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit (TIM7, &TIMER_TimeBaseInitStructure);

    TIM_ITConfig (TIM7, TIM_IT_Update, ENABLE);

    NVIC_InitStructure.NVIC_IRQChannel = TIM7_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init (&NVIC_InitStructure);

    TIM_Cmd (TIM7, ENABLE);
}

Para este caso, tenemos programada la frecuencia del contador a 10 KHz (hay que tener en cuenta que este timer tiene un PLL interno que dobla la frecuencia de entrada, siendo esta de la del APB1, 42 MHz)

$${ TIMx }_{ psc }=\frac { { CK }_{ psc } }{ CK_{ cnt } } -1=\frac { 84000000 }{ 10000 } -1=8400-1$$

Para obtener una animación de los leds apreciable al ojo humano, programaremos la interrupción del timer con un periodo de 1 segundo, de modo que el valor que habremos de cargar es 10000 - 1. Para facilitar el manejo de este valor, el contador se configura en modo ascendente, de forma que cuando este llega al valor cargado vuelve automáticamente a cero y comienza de nuevo la cuenta.
Dicha animación se ejecuta en la rutina de atención a la interrupción del TIM7 como se ve a continuación  (el bucle principal del microcontrolador se encuentra vacío):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void TIM7_IRQHandler (void)
{
    if (TIM_GetITStatus (TIM7, TIM_IT_Update ) != RESET)
    {
        if (GPIO_ReadOutputDataBit (GPIOD, GPIO_Pin_12) == Bit_SET)
        {
            GPIO_SetBits (GPIOD, GPIO_Pin_13);
            GPIO_ResetBits (GPIOD, GPIO_Pin_12);
        }
        else if (GPIO_ReadOutputDataBit (GPIOD, GPIO_Pin_13) == Bit_SET)
        {
            GPIO_SetBits (GPIOD, GPIO_Pin_14);
            GPIO_ResetBits (GPIOD, GPIO_Pin_13);
        }
        else if (GPIO_ReadOutputDataBit (GPIOD, GPIO_Pin_14) == Bit_SET)
        {
            GPIO_SetBits (GPIOD, GPIO_Pin_15);
            GPIO_ResetBits (GPIOD, GPIO_Pin_14);
        }
        else if (GPIO_ReadOutputDataBit (GPIOD, GPIO_Pin_15) == Bit_SET)
        {
            GPIO_SetBits (GPIOD, GPIO_Pin_12);
            GPIO_ResetBits (GPIOD, GPIO_Pin_15);
        }
    TIM_ClearITPendingBit (TIM7, TIM_IT_Update);
    }
}

Aquí simplemente se detecta que led esta encendido para pasar a activar el siguiente y apagar el actual.

A continuación podemos ver un pequeño vídeo donde se ve el efecto de esta animación.


Y finalmente, el proyecto para importar a SW4STM32: TIMER Interrupt STM32F4 Discovery Dropbox


Vista la funcionalidad genérica de los timers, pasamos a ver otro ejemplo, en este caso sobre PWM, en el que se irá variando la intensidad lumínica de un led auxiliar (se aprecia mejor que directamente sobre los leds de la placa).

Para este ejemplo haremos uso del TIM4. También nos valdremos de la interrupción programada anteriormente para variar progresivamente el ancho del pulso del PWM.
Así pues, lo primero que haremos será configurar los puertos de entrada y salida.  En este caso usaremos solo el pin del led azul, GPIO_Pin_15, en modo función alternativa, como se ve a continuación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void GPIO_Configure (void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_AHB1PeriphClockCmd (RCC_AHB1Periph_GPIOD, ENABLE);

    GPIO_PinAFConfig (GPIOD, GPIO_PinSource15 , GPIO_AF_TIM4);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_Init (GPIOD, &GPIO_InitStructure);
}

Con esto conseguimos que la salida output compare 4 del timer en cuestión se configure para este cometido y podemos con ello ver el efecto sobre el led conectado al mismo pin.
Lo siguiente será establecer la configuración del TIM4 para la generación del PWM. Antes de ver el codigo correspondiente a este aspecto, veremos una imagen donde se muestra el funcionamiento del output compare.


Fuente: PWM

Como podemos ver, y veremos a continuación, la linea roja de la gráfica superior se corresponde con el valor que cargaremos en el registro del contador (en nuestro caso 8400 - 1) y la linea en verde es el ancho del pulso del PWM (su valor máximo sera el anterior). El funcionamiento es muy sencillo: cuando el contador supera el valor escogido para el ancho del pulso, la salida  digital cambia de estado para generar el PWM y vuelve a cambiar con el final de cuenta para completar el pulso. Seguidamente se muestra el código implementado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void TIMER_Configure (void)
{
    TIM_TimeBaseInitTypeDef TIMER_TimeBaseInitStructure;
    TIM_OCInitTypeDef  TIMER_OCInitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB1PeriphClockCmd (RCC_APB1Periph_TIM7, ENABLE);

    TIMER_TimeBaseInitStructure.TIM_Prescaler = 8400 - 1;
    TIMER_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIMER_TimeBaseInitStructure.TIM_Period = 10000 - 1;
    TIMER_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIMER_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit (TIM7, &TIMER_TimeBaseInitStructure);

    TIM_ITConfig (TIM7, TIM_IT_Update, ENABLE);

    NVIC_InitStructure.NVIC_IRQChannel = TIM7_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init (&NVIC_InitStructure);

    TIM_Cmd (TIM7, ENABLE);

    RCC_APB1PeriphClockCmd (RCC_APB1Periph_TIM4, ENABLE);

    TIMER_TimeBaseInitStructure.TIM_Prescaler = 0;
    TIMER_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIMER_TimeBaseInitStructure.TIM_Period = 8400 - 1;
    TIMER_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIMER_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit (TIM4, &TIMER_TimeBaseInitStructure);

    TIMER_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2;
    TIMER_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIMER_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;
    TIMER_OCInitStructure.TIM_Pulse = 1 - 1;
    TIM_OC4Init (TIM4, &TIMER_OCInitStructure);
    TIM_OC4PreloadConfig (TIM4, TIM_OCPreload_Enable);

    TIM_Cmd (TIM4, ENABLE);
}

Inicialmente dejaremos intacta la configuración del TIM7 para, en su interrupción, cambiar el valor del ancho del pulso. También podríamos, en vez de reutilizar la interrupción, cambiar el valor del ancho del pulso del PWM cada vez que se pulse el botón de usuario, esto se deja comentado para quien quiera probarlo.

Para el TIM4 mantenemos la frecuencia de entrada para el contador, 84 MHz, y es en el registro de este donde determinamos el ancho máximo del pulso del PWM. Tras esto configuramos el PWM. El aspecto mas interesante aquí es la elección de la polaridad de comparación, de modo que obtenemos la forma de la segunda gráfica de la imagen anterior al seleccionar TIM_OCPolarity_Low (TIM_OCPolarity_High genera la forma de la tercera gráfica). Finalmente establecemos un valor para el ancho del pulso.

Como dijimos antes, la variación del ancho del pulso lo haremos mediante la interrupción del TIM7 como se ve a continuación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void TIM7_IRQHandler (void)
{
    static int i = 0;
    if (TIM_GetITStatus (TIM7, TIM_IT_Update ) != RESET)
    {
        TIM_SetCompare4 (TIM4, pulse [i]);

        i++;
        if (i == 9)
     i = 0;

 TIM_ClearITPendingBit (TIM7, TIM_IT_Update);
    }
}

Donde

1
uint32_t pulse [9] = {0, 1050 - 1, 2100 - 1, 3150 - 1, 4200 - 1, 5250 - 1, 6300 - 1, 7350 - 1, 8400 - 1};

Simplemente iremos incrementando el indice del vector anterior, con cada iteración de la interrupción, para conseguir la variación del PWM que podemos apreciar en el siguiente vídeo. Dado que en el led de la placa era difícil de apreciar, y mas aun en la grabación, se ha colocado un led de alta luminosidad en paralelo para poder observar el efecto.


Y de nuevo, el proyecto listo para importar: TIMER Pwm STM32F4 Discovery Dropbox

Como ya dije anteriormente, existen otras funcionalidades también interesantes que requieren de hardware externo para poderlas comprobar, así que si dispongo de estos, me gustaría continuar actualizando esta entrada. De mientras continuaré realizando estos pequeños post sobre los demás periféricos disponibles.