STM32F4: Cómo utilizar el ADC con DMA

Para este post, continuando con los post de esta placa, veremos los primeros pasos a dar para comenzar a obtener datos del ADC utilizando el DMA.

Pero la primera pregunta que debemos contestar es ¿qué es el DMA? El DMA (Direct Memory Access) es una forma de leer o escribir en memoria sin utilizar la CPU de la que disponen ciertos elementos como el ADC. Esto significa que no estamos perdiendo tiempo de CPU en procesar las muestras, por lo que éstas se procesan en background y mientras podemos estar haciendo otra cosa. Además, esto permite que las muestras de una señal no se pierdan por tiempo de procesado de la CPU.

CONFIGURACIÓN INICIAL

Abrimos CubeMX y habilitamos el ADC como en pasados tutoriales, pero esta vez habilitamos la opción de DMA Continuous Requests.

Configuración ADC

En ese momento, en la pestaña de DMA Settings tenemos que añadir un stream que tenga las características que se muestran en la imagen y después comprobar que en NVIC Settings de éste está habilitado. Es importante prestar atención al tipo de buffer a utilizar. Para esta primera prueba utilizaremos el normal, y después pasaremos al circular, puesto que es el que nos permite tener una captación de datos continua y renovada.

Ajustes para usar el DMA con el ADC

Tenemos que mantener el timer interno funcionando como en el tutorial anterior, ya que así garantizamos que estamos tomando las muestras correctamente, pero también se puede utilizar el ADC con DMA sin timer.

LECTURA Y REPRESENTACIÓN DE LA SEÑAL

Empezamos arrancando el timer y el ADC, pero para este último, utilizando las funciones que están relacionadas también con el DMA (como HAL_ADC_Start_DMA). Esta última función tiene como uno de sus argumentos el tamaño de los datos que vamos a leer.

Tras esto, haciendo uso de las funciones relacionadas con el DMA y el ADC, podemos controlar cuando ha terminado la lectura para, en ese momento, guardar los datos. De hecho, podemos controlar tanto la transacción completa como la mitad de la misma. Se deja la implementación al lector.

Finalmente, representamos estos datos para saber si lo estamos haciendo bien, (considerando que la configuración del timer es la misma que en el ejemplo anterior y la señal de entrada también). Manteniendo esta configuración y cambiando el buffer del DMA a circular en adc.c (hdma_adc1.Init.Mode = DMA_CIRCULAR;), lo que conseguimos es que el array donde se actualizan los datos esté en continuo cambio, pero hasta que no se detecta el fin de la transacción, no se guarda. La representación sería la misma en los dos casos (normal y circular).

Representación de los datos procesados por el DMA del ADC

 

Nota: la imagen de la portada se ha obtenido de CocoaStream Technologies.

STM32F4: Configuración del ADC con un timer

Este tutorial debe seguirse después de haber realizado el tutorial sobre la configuración y el uso básico del ADC, el cual se encuentra en la lista de tutoriales de la placa de desarrollo STM32F4.

 

CONFIGURACIÓN INICIAL

Timer externo:

En este caso, en CubeMX, también seleccionamos el pin PA1 como pin de entrada al ADC, pero además, en las opciones de la izquierda que se muestran al desplegar el ADC1, debemos seleccionar External-Trigger-for-Regular-Conversion. Esto se debe a que vamos a utilizar un timer para controlar cuando realizamos las lecturas del ADC. Por lo tanto, tenemos que habilitar este timer también. Podría ser cualquiera de los que muestra cubeMX, nosotros hemos elegido el timer 1, canal 1. Entre las opciones posibles para su configuración, hemos elegido PWM generation CH1, para así utilizar la información de uno de los anteriores tutoriales, aunque ésta no sería la única opción.

Todo esto puede verse reflejado en la imagen siguiente, donde encontramos los pines ADC1_TIM1, TIM1_CH1 y ADC1_EXTI11, la casilla de External-Trigger-for-Regular-Conversion marcada y el timer configurado como PWM.

Pinout ADC – PWM Timer

Al activar estas opciones, la configuración del ADC y del timer es la que encontramos en las imágenes siguientes. En cuanto al timer, hemos impuesto una frecuencia de 1 KHz y un ciclo del 50%. Sabiendo que la frecuencia del sistema es de 96 MHz, nos queda la siguiente igualdad:

(Preescaler + 1)x(Periodo + 1) = fclk/fPWM

Donde suponemos el preescalado y el periodo y obtenemos el pulso.

Configuración del ADC y del PWM Timer

Se deja al lector la implementación del timer externo sin configuración PWM.

Timer interno:

Para este otro caso, seleccionamos un timer que no tenga muchas funcionalidades, como el timer 2 (TIM2). En la opción clock source, seleccionamos Internal Clock, y después en la pestaña de configuración, ponemos la misma configuración que teníamos con el PWM en los campos que sean iguales.
Es importante seleccionar en “Trigger Event Selection TRGO” la opción de Update Event.

 

LECTURA Y REPRESENTACIÓN DE LA SEÑAL

Timer externo:

Ahora conectaremos el pin asociado al TIM1_CH1 con el pin asociado al ADC1_EXTI11 con un cable externo, y el generador de funciones en la entrada del ADC (PA1). Estas señales se pueden visualizar en el osciloscopio para comprobar que efectivamente es como esperamos.
Pasamos la configuración de CubeMX a Eclipse. Ahora no solo tenemos que iniciar el ADC sino también el timer. Después, la toma de datos sería de la misma forma que en el anterior tutorial. Se deja al lector la tarea de implementar el código, sería necesario el código del tutorial anterior del ADC y del tutorial relacionado con el timer.

Sin embargo, aquí tenemos que tener en cuenta el teorema de Nyquist, puesto que ahora tenemos una frecuencia de muestreo conocida. La señal de entrada será de 50 Hz, ya que así dispondremos de 20 muestras por período. El array en el que se guardan será de 100 muestras, por lo que tendremos 5 períodos de una representación muy aproximada de la señal, como bien puede observarse en la imagen.

Señal de 1 KHz procesada por el ADC
Timer interno:

En este caso no sería necesario conectar ningún cable adicional.
Tras pasar la configuración a Eclipse, iniciamos el timer de una forma diferente a la anterior, utilizando la función HAL_TIM_Base_Start(&htim2). Y ya podríamos correr nuestro código.

 

Nota: la imagen de la portada se ha obtenido de Jóvenes Científicos