Nivel: Avanzado
¿Qué tengo que saber para este post?
- Tener conocimientos sobre programación en C.
- Tener experiencia programando interrupciones.
- Saber lo que es un DAC con Red R2R.
- Saber como utilizar una LookUp Table o tabla en un µC (no excluyente, pero recomendable).
- Tener conocimientos sobre un DMA (Direct Memory Access) te facilitará la lectura, aunque pienso explicarlo por arriba.
—————————————
¿Qué es un DMA?
Antes de arrancar, para muchos esta puede ser la primera vez que escuchen sobre un DMA (Direct Memory Access), y ¿qué mierda es un DMA?¿por qué hay DOS a falta de uno en el STM32F103? En realidad es menos complejo el concepto de lo que creen. Como sus siglas en inglés lo indican es un módulo de hardware que accede directamente a la memoria sin la intervención del CPU. Así, como lo leen, un DMA puede tomar un cierto espacio de memoria y trasladarlo a otro (o a un periférico de E/S) sin la necesidad de que el procesador se entere de nada. Esto aumenta la velocidad considerablemente, ya que por un lado el CPU queda libre para hacer otras cosas procesar otros datos, mientras que el DMA mueve bytes de aquí para allá sin tener que esperar, salvo por la liberación del bus. Porque si bien, en algunas arquitecturas podría tener su propio bus, en general el bus de datos/direcciones es compartido y el DMA y el CPU deben administrarse el recurso.
Los DMA tienen canales (channels) que son los encargados de mover de un lugar a otro los datos, es decir que un DMA puede estar programado para hacer varias transferencias desde y hacia diferentes espacios de memoria. Pero nuevamente, por una cuestión de recursos solo un canal a la vez puede acceder al bus. Puede ser, como en nuestro caso, que estos canales tengan diferentes prioridades, es decir que un canal tiene preponderancia sobre el otro en caso de que ambos necesiten acceder al mismo tiempo.
Los DMA en el STM32F103
Como ya les adelanté, hay 2 DMA en este µControlador, el DMA1
que posee 7 canales y el DMA2
que posee 5 canales. Como se observa en el gráfico a continuación sacado del reference manual de ST, se destaca en verde cómo estos acceden a través del AHB a los periféricos y a través del Bus matrix a la RAM (¡¡o Flash también!!). Recordemos que los canales son los encargados de transferir los datos, es decir que los podemos programar individualmente. Además podemos setear por software la prioridad de estos canales y si hay dos canales con la misma prioridad el de menor número será el más prioritario, siendo el canal 1 el que tienen mayor prioridad. Sobre los DMA request hablaré más adelante (lineas amarillas).
El DMA1
y DMA2
, desde el punto de vista del programador, deben entenderse como un módulo/periférico más dentro del µControlador, y poseen registros de funciones especiales (SFR) para su configuración y funcionamiento como todos los demás. Pero debemos entender que es uno de los periféricos más avanzados que poseemos a nuestra disposición. Recordemos que este Manual de Referencia es para la linea STM32F1xx, hay periféricos que no se encuentran en la BluePill.
Eventos vs. Interrupciones
Como sabemos las interrupciones son señales que se envían al CPU (denominadas IRQ – Interrupt ReQuest) para indicarle que debe hacer un cambio en el flujo del programa para atender a quienquiera que haya generado esa señal. Como les dije anteriormente, el DMA es un periférico más, pero tiene la facultad de recibir o activarse por una señal generada por otro periférico. A esto último se lo denomina Evento.
Los eventos que nos interesan para este post son los que generan un DMA request, que son los que «disparan» la transferencia de datos, y estaban marcados en amarillo en el gráfico anterior. Dependiendo cuál sea el canal, los diversos periféricos pueden generar este evento, por lo cual debemos tener presentes las configuraciones de los mismos. Aquí les dejo la tabla de los eventos que pueden generar un inicio de transferencia.
No es estrictamente necesario un evento para empezar la transferencia de datos, tenemos un modo denominado M2M (Memory-to-Memory) que podemos «arrancar» por software. Hay un ejemplo de esto en el repositorio de libOpenCM3 que se usa para copiar un string a otro. Si saltaron directo a eso y no entendieron ni mierda nada, no desesperen ahora voy a intentar dar algunas pautas para entender. Pero para ello debemos conocer algunos detalles.
¿Cómo se programa esta cosa?
Debemos recorrer algunas de las características que soportan los DMA del STM32F103 para entender cómo programarlos. Por ejemplo, indistintamente de si se trata de direcciones de memoria o periféricos, el origen-destino se llaman memory(address)
y peripheral(address)
.
¿De dónde a dónde?
Como les comentaba, debemos saber de dónde (qué dirección de memoria) a dónde vamos a hacer la transferencia. Para ello definiremos de donde vamos a leer utilizando alguna de las siguientes funciones disponibles en libOpenCM3, donde dma
se completa con DMA1
o DMA2
y channel
se reemplaza con DMA_CHANNELx
donde x
es un número del 1 al 7 para el DMA1 y del 1 al 5 para el DMA2:
void dma_read_from_memory(uint32_t dma, uint8_t channel);
void dma_read_from_peripheral(uint32_t dma, uint8_t channel);
Les quiero hacer recordar que por más que la función se llame «peripheral», no deja de ser una dirección de memoria, así que podría ser de la memoria a la memoria. En el caso especial de que no quisiéramos esperar un evento para inicializar la transferencia, podemos utilizar el modo «memoria a memoria» que inicializará apenas habilitemos el canal del DMA:
void dma_enable_mem2mem_mode(uint32_t dma, uint8_t channel);
Pero claro que no alcanza solo con indicar para dónde es el flujo de los datos, sino dónde están, es decir las direcciones de memoria. Para ello debemos utilizar:
void dma_set_memory_address(uint32_t dma, uint8_t channel, uint32_t address);
void dma_set_peripheral_address(uint32_t dma, uint8_t channel, uint32_t address);
Tamaño de los datos a transferir
El DMA puede transferir diferentes tamaños de información a la vez, es decir que puede transferir datos de 8, 16 o 32 bits. Esto es programable tanto de la memoria como del periférico usando las siguientes funciones y defines para los parámetros memory_size
y peripheral_size
. Así son las funciones de libOpenCM3:
#define DMA_CCR_MSIZE_8BIT
#define DMA_CCR_MSIZE_16BIT
#define DMA_CCR_MSIZE_32BIT
void dma_set_memory_size(uint32_t dma, uint8_t channel, uint32_t mem_size);
#define DMA_CCR_PSIZE_8BIT
#define DMA_CCR_PSIZE_16BIT
#define DMA_CCR_PSIZE_32BIT
void dma_set_peripheral_size(uint32_t dma, uint8_t channel, uint32_t peripheral_size);
¿Cuántos datos?¿cómo?
Ya sabemos de dónde a dónde y el tamaño de los datos, pero debemos saber cuántos datos vamos a transferir de la posición de origen a la o las direcciones de memoria destino, que tiene un máximo de 65536. Lo cual abre además 2 posibilidades que por cada transferencia el puntero del DMA incremente o no, tanto a partir de la dirección de memoria como la del periférico. Y para ello, tenemos las siguientes funciones:
void dma_set_number_of_data(uint32_t dma, uint8_t channel, uint16_t number);
void dma_enable_memory_increment_mode(uint32_t dma, uint8_t channel);
void dma_disable_memory_increment_mode(uint32_t dma, uint8_t channel);
void dma_enable_peripheral_increment_mode(uint32_t dma, uint8_t channel);
void dma_disable_peripheral_increment_mode(uint32_t dma, uint8_t channel);
Además, podemos activar un modo circular, es decir que cuando el DMA termine de transferir los datos, el puntero volver a empezar desde el principio de los datos. Para activar este modo utilizamos la función:
void dma_enable_circular_mode(uint32_t dma, uint8_t channel);
Prioridad e interrupciones
Como anticipé, se puede setear prioridades a cada canal por software, a decir verdad, 4 niveles de prioridad, recordando que los canales con número más pequeño son más prioritarios en caso de empatar. Para ello disponemos de la siguiente función y defines para el parámetro prio
:
#define DMA_CCR_PL_LOW
#define DMA_CCR_PL_MEDIUM
#define DMA_CCR_PL_HIGH
#define DMA_CCR_PL_VERY_HIGH
void dma_set_priority(uint32_t dma, uint8_t channel, uint32_t prio);
Como casi todos los periféricos, este puede generar interrupciones, en este caso son 3. Una interrupción puede generarse cuando el canal del DMA está a la mitad de los datos transferidos (1), cuando se terminaron de transferir(2) o si ocurrió un error en medio de la transferencia(3). Cada canal tiene su propio vector de interrupción siendo las anteriormente mencionadas las fuentes que activan alguno de los 3 flags correspondientes.
// (1)
void dma_enable_half_transfer_interrupt(uint32_t dma, uint8_t channel);
void dma_disable_half_transfer_interrupt(uint32_t dma, uint8_t channel);
// (2)
void dma_enable_transfer_complete_interrupt(uint32_t dma, uint8_t channel);
void dma_disable_transfer_complete_interrupt(uint32_t dma, uint8_t channel);
// (3)
void dma_enable_transfer_error_interrupt(uint32_t dma, uint8_t channel);
void dma_disable_transfer_error_interrupt(uint32_t dma, uint8_t channel);
Las ISR tienen el formato void dmaX_channelY_isr(void)
donde X
es 1 ó 2 e Y
el canal del 1-7 ó 1-5 según corresponda. Una vez en la subrutina de interrupción podemos saber de cual fue la fuente leyendo los flags y recordando bajarlos. Y para ello, podemos utilizar los defines para identificarlas y las funciones:
#define DMA_FLAGS // todos los flags
#define DMA_HTIF // Half Transfer Interrupt Flag
#define DMA_TCIF // Transfer Complete Interrupt Flag
#define DMA_TEIF // Transfer Error Interrupt Flag
bool dma_get_interrupt_flag(uint32_t dma, uint8_t channel, uint32_t interrupt);
void dma_clear_interrupt_flags(uint32_t dma, uint8_t channel, uint32_t interrupts);
Recordando que debemos habilitar en el NVIC las interrupciones que queramos utilizar con el define correspondiente según el formato NVIC_DMAx_CHANNELy_IRQ
donde x
se reemplaza por 1 ó 2 e y
del 1-7 ó 1-5 según corresponda. Recordando que debemos utilizar la función:
nvic_enable_irq (uint8_t irqn);
Control y habilitación
Ya configuramos todo el DMA, así que solo falta habilitarlo. Debemos considerar que si queremos reconfigurar o cambiar alguna configuración, primero debemos inhabilitar el DMA antes de cambiar algo. Y si quisiéramos, también podemos resetearlo, es decir que inhabilita el canal y vuelve todos los registros de configuración a 0 (cero).
void dma_enable_channel(uint32_t dma, uint8_t channel);
void dma_disable_channel(uint32_t dma, uint8_t channel);
void dma_channel_reset(uint32_t dma, uint8_t channel);
¡Cuidado! Si está configurado el modo «Memoria a Memoria» al habilitar el DMA empezará inmediatamente la transferencia. Ahora, si no es así, debemos ir al periférico que generará el evento que iniciará la transferencia y configurarlo. Para ello, debemos volver a las tablas que puse al principio para saber qué canal utilizar y qué periférico deseamos utilizar en conjunto con el DMA.
Estamos listos para el ejemplo (Generador de una señal senoidal de 10KHz)
Para el siguiente ejemplo de código, vamos a utilizar el desborde (Update Event) del TIM4 para inicializar las transferencias del canal 7 del DMA1 que moverá valores desde una posición de memoria que almacena una LUT de 144 posiciones con valores de 8 bits de una señal senoidal –generada con mi software para tal propósito- y los transferirá a el ODR (Output Data Register) del GPIOA en los pines del PA0 al PA7. Estos pines están conectados a una Red R2R para poder visualizarlo en el osciloscopio.
Bueno, lo primero que necesitamos es la tabla, lo cual ya tengo a través del soft:
/*const*/ uint8_t sineLUT[] = {
0x80,0x85,0x8B,0x90,0x96,0x9B,0xA0,0xA6,
0xAB,0xB0,0xB5,0xBA,0xBF,0xC4,0xC9,0xCD,
0xD1,0xD6,0xDA,0xDE,0xE1,0xE5,0xE8,0xEB,
0xEE,0xF1,0xF3,0xF5,0xF7,0xF9,0xFB,0xFC,
0xFD,0xFE,0xFF,0xFF,0xFF,0xFF,0xFF,0xFE,
0xFD,0xFC,0xFB,0xF9,0xF7,0xF5,0xF3,0xF1,
0xEE,0xEB,0xE8,0xE5,0xE1,0xDE,0xDA,0xD6,
0xD1,0xCD,0xC9,0xC4,0xBF,0xBA,0xB5,0xB0,
0xAB,0xA6,0xA0,0x9B,0x96,0x90,0x8B,0x85,
0x80,0x7A,0x74,0x6F,0x69,0x64,0x5F,0x59,
0x54,0x4F,0x4A,0x45,0x40,0x3B,0x36,0x32,
0x2E,0x29,0x25,0x21,0x1E,0x1A,0x17,0x14,
0x11,0x0E,0x0C,0x0A,0x08,0x06,0x04,0x03,
0x02,0x01,0x00,0x00,0x00,0x00,0x00,0x01,
0x02,0x03,0x04,0x06,0x08,0x0A,0x0C,0x0E,
0x11,0x14,0x17,0x1A,0x1E,0x21,0x25,0x29,
0x2E,0x32,0x36,0x3B,0x40,0x45,0x4A,0x4F,
0x54,0x59,0x5F,0x64,0x69,0x6F,0x74,0x7A
};
Si descomentamos el const
, la tabla quedará en la memoria de programa y si la dejamos comentada esta se cargará al inicio en la RAM. Luego debemos habilitar el clock en el RCC del GPIOA y configurar los pines del PA0 al PA7 como salidas:
rcc_periph_clock_enable(RCC_GPIOA);
gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO0_TO_7);
Ahora viene lo bueno jóvenes. Vamos a configurar el DMA con todo lo que vimos anteriormente. Como la idea es que el UpdateEvent sea el que inicie la transferencia, utilizaremos el canal 7 del DMA1
que es el que está asociado a éste. Obviamente, antes de empezar debemos habilitar el clock en el RCC
del DMA1
. Lo siguiente que haremos es definir que la transferencia será de la memoria al periférico, de qué dirección a cual otra, que los datos serán de 8 bits y que serán 144 datos.
// Se habilita el clock del periferico DMA1
rcc_periph_clock_enable(RCC_DMA1);
// Se indica que es de MEM -> Periférico
dma_set_read_from_memory(DMA1, DMA_CHANNEL7);
// Origen de los datos a transferir:
dma_set_memory_address(DMA1, DMA_CHANNEL7, (uint32_t)sineLUT);
// Dirección de datos destino. El ODR (Output Data Register) del GPIOA:
dma_set_peripheral_address(DMA1, DMA_CHANNEL7, (uint32_t)&GPIO_ODR(GPIOA));
// Tamaño del dato a leer:
dma_set_memory_size(DMA1, DMA_CHANNEL7, DMA_CCR_MSIZE_8BIT);
// Tamaño del dato a escribir:
dma_set_peripheral_size(DMA1, DMA_CHANNEL7, DMA_CCR_PSIZE_8BIT);
// Cantidad de datos a transferir:
dma_set_number_of_data(DMA1, DMA_CHANNEL7, 144);
Como lo que queremos hacer es que los datos de la tabla se vayan escribiendo de a uno en el ODR del GPIOA, debemos mantener este último fijo, pero las direcciones de memoria deben ir incrementando. A su vez, queremos que cuando se termine la tabla volver al principio. Por lo tanto nos convendría utilizar el modo circular. Solo para jugar, porque no habrá otro periférico ni nada más ejecutándose, vamos a setear la prioridad del canal como la más alta.
// Se incrementa automaticamente la posición en memoria:
dma_enable_memory_increment_mode(DMA1, DMA_CHANNEL7);
// La dirección destino se mantiene fija:
dma_disable_peripheral_increment_mode(DMA1, DMA_CHANNEL7);
// Se habilita la función de buffer circular:
dma_enable_circular_mode(DMA1, DMA_CHANNEL7);
// Se establece la prioridad del canal 7 del DMA1 como alta:
dma_set_priority(DMA1, DMA_CHANNEL7, DMA_CCR_PL_VERY_HIGH);
Finalmente, voy a habilitar la interrupción de transferencia completa, porque allí pienso hacer un toggle del PC13 (LED de la BluePill) para utilizarlo como señal de trigger en mi osciloscopio. Y luego de todo esto, habilitaré el canal 7 del DMA1
. Pero cabe destacar que con el trigger automático de los osciloscopios esto en realidad no es necesario.
// Se habilita la interrupción que se ejecutan al finalizar la transferencia para togglear un pin (no es necesario)
dma_enable_transfer_complete_interrupt(DMA1, DMA_CHANNEL7);
// Habilitación del canal:
dma_enable_channel(DMA1, DMA_CHANNEL7);
Con eso finaliza la configuración del DMA… naaah mentira, todavía falta la otra parte, la que no les conté. Debemos configurar el otro periférico para que genere el DMA request. Simplemente voy a configurar el TIM4 para desbordar con una cuenta a 1.44MHz… esto ya lo hemos visto en un post anterior, por eso voy a dirigirme específicamente a lo relevante para el DMA.
Debemos habilitar el evento de DMA cuando se actualiza la cuenta (Update Event), para ello, ya hay una función en libOpenCM3 que nos facilita la cuestión. Pero además debemos habilitarlo como si se tratase de una interrupción utilizando el define correspondiente:
// Se habilita el evento de DMA en el overflow
timer_set_dma_on_update_event(TIM4); // DMA1 channel-7
timer_enable_irq(TIM4, TIM_DIER_UDE); // UpdateEvent
Luego solo queda habilitar el timer y habilitar la interrupción ya que esto es esencial para poder togglear el pin como les mencioné. Esto último se vereía así:
// Se habilita en el NVIC la interrupción del DMA1-CH7
nvic_enable_irq(NVIC_DMA1_CHANNEL7_IRQ);
El código completo e imágenes
A continuación les dejo el código completo para que lo puedan estudiar y además unas imágenes y tips para poder probarlo.
#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/stm32/timer.h>
#include <libopencm3/stm32/dma.h>
#include <libopencm3/cm3/systick.h>
#include <libopencm3/cm3/nvic.h>
#define GPIO0_TO_7 0x00FF
#define GPIO8_TO_15 0xFF00
const uint8_t sineLUT[] = {
0x80, 0x85, 0x8B, 0x90, 0x96, 0x9B, 0xA0, 0xA6,
0xAB, 0xB0, 0xB5, 0xBA, 0xBF, 0xC4, 0xC9, 0xCD,
0xD1, 0xD6, 0xDA, 0xDE, 0xE1, 0xE5, 0xE8, 0xEB,
0xEE, 0xF1, 0xF3, 0xF5, 0xF7, 0xF9, 0xFB, 0xFC,
0xFD, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE,
0xFD, 0xFC, 0xFB, 0xF9, 0xF7, 0xF5, 0xF3, 0xF1,
0xEE, 0xEB, 0xE8, 0xE5, 0xE1, 0xDE, 0xDA, 0xD6,
0xD1, 0xCD, 0xC9, 0xC4, 0xBF, 0xBA, 0xB5, 0xB0,
0xAB, 0xA6, 0xA0, 0x9B, 0x96, 0x90, 0x8B, 0x85,
0x80, 0x7A, 0x74, 0x6F, 0x69, 0x64, 0x5F, 0x59,
0x54, 0x4F, 0x4A, 0x45, 0x40, 0x3B, 0x36, 0x32,
0x2E, 0x29, 0x25, 0x21, 0x1E, 0x1A, 0x17, 0x14,
0x11, 0x0E, 0x0C, 0x0A, 0x08, 0x06, 0x04, 0x03,
0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x02, 0x03, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E,
0x11, 0x14, 0x17, 0x1A, 0x1E, 0x21, 0x25, 0x29,
0x2E, 0x32, 0x36, 0x3B, 0x40, 0x45, 0x4A, 0x4F,
0x54, 0x59, 0x5F, 0x64, 0x69, 0x6F, 0x74, 0x7A
};
int main(void)
{
rcc_clock_setup_in_hse_8mhz_out_72mhz();
// Salida para el DAC R2R de 8 bits del PA0-PA7
rcc_periph_clock_enable(RCC_GPIOA);
gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO0_TO_7);
// Toggle del PC13 como trigger (no es necesario para el funcionamiento del DMA)
rcc_periph_clock_enable(RCC_GPIOC);
gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO13);
gpio_set(GPIOC, GPIO13);
// Se habilita el clock del periferico DMA1
rcc_periph_clock_enable(RCC_DMA1);
// Se indica que es de MEM -> Periférico
dma_set_read_from_memory(DMA1, DMA_CHANNEL7);
// Origen de los datos a transferir:
dma_set_memory_address(DMA1, DMA_CHANNEL7, (uint32_t)sineLUT);
// Dirección de datos destino. El ODR (Output Data Register) del GPIOA:
dma_set_peripheral_address(DMA1, DMA_CHANNEL7, (uint32_t)&GPIO_ODR(GPIOA));
// Tamaño del dato a leer:
dma_set_memory_size(DMA1, DMA_CHANNEL7, DMA_CCR_MSIZE_8BIT);
// Tamaño del dato a escribir:
dma_set_peripheral_size(DMA1, DMA_CHANNEL7, DMA_CCR_PSIZE_8BIT);
// Cantidad de datos a transferir:
dma_set_number_of_data(DMA1, DMA_CHANNEL7, 144);
// Se incrementa automaticamente la posición en memoria:
dma_enable_memory_increment_mode(DMA1, DMA_CHANNEL7);
// La dirección destino se mantiene fija:
dma_disable_peripheral_increment_mode(DMA1, DMA_CHANNEL7);
// Se habilita la función de buffer circular:
dma_enable_circular_mode(DMA1, DMA_CHANNEL7);
// Se establece la prioridad del canal 7 del DMA1 como alta:
dma_set_priority(DMA1, DMA_CHANNEL7, DMA_CCR_PL_VERY_HIGH);
// Se habilita la interrupción que se ejecutan al finalizar
// la transferencia para togglear un pin (no es necesario)
dma_enable_transfer_complete_interrupt(DMA1, DMA_CHANNEL7);
// Habilitación del canal:
dma_enable_channel(DMA1, DMA_CHANNEL7);
// Se habilita el clock del periférico del TIM4
rcc_periph_clock_enable(RCC_TIM4);
// Se configura el modo de funcionamiento del Timer4
timer_set_mode(
TIM4, // Timer4
TIM_CR1_CKD_CK_INT, // fuente Clk interno
TIM_CR1_CMS_EDGE, // Alineado por flanco
TIM_CR1_DIR_UP); // Cuenta ascendente
timer_set_prescaler(TIM4, 0); // 72MHz / 1 => 72MHz
// Se setea el valor hasta donde se cuenta:
timer_set_period(TIM4, 49); // 72MHz / 50 = 1.44MHz => 1.44M/144 = 10kHz
// Se habilita el evento del DMA en el overflow
timer_set_dma_on_update_event(TIM4); // DMA1 channel7
timer_enable_irq(TIM4, TIM_DIER_UDE);
// Empieza a contar el Timer4
timer_enable_counter(TIM4);
// Se habilita en el NVIC la interrupción del DMA1-CH7
nvic_enable_irq(NVIC_DMA1_CHANNEL7_IRQ);
while (true)
; // NADA el DMA + Timer4 manejan todo
}
/**
* @brief Solo está para generar un pulso de "trigger"
*
*/
void dma1_channel7_isr(void)
{
// Se bajan los flags:
dma_clear_interrupt_flags(DMA1, DMA_CHANNEL7, DMA_FLAGS);
// Se togglea el pin (LED)
gpio_toggle(GPIOC, GPIO13);
}
Acá les dejo algunas imágenes para que puedan ver el resultado:
Si no tienen este instrumento en casa, como yo que no tenía hasta que me prestaron, pueden bajarse una app de osciloscopio y en combinación con un cablecito fácil de adquirir que separa el micrófono y los auriculares pueden sacar imágenes más que interesantes. Si su jack de laptop tiene para micrófono lo soporta también pueden utilizar el software xoscope, no es de lo más lindo, pero al menos pueden hacer alguna medición.
Para terminar, les dejo a modo de desafío hacer un sweep de frecuencia (de 500Hz a 10Khz) como el que les dejo en el siguiente video. A ver si se les ocurre como lo hice (es fácil).
Los saludo hasta la próxima y espero que este post haya valido la pena el esfuerzo que me tomó escribirlo. Hasta el próximo.
Federico
Muy claro y didáctico, gracias por la dedicación. Saludos