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.