Sistema PCM entre ESP32

A continuación encontrarás un bosquejo en español de la solución, cubriendo la descripción conceptual, el diagrama de bloques, los cálculos básicos de SNR y distorsión, y un ejemplo de código (solo a modo de referencia) para cada microcontrolador ESP32 usando Arduino IDE. Se han dividido las secciones en (A) Adquisición y Compresión, (B) Transmisión, (C) Recepción y Descompresión, y (D) DAC y Salida al Parlante. Finalmente, se incluyen tablas con parámetros ajustables y las fórmulas aproximadas de cuantización y mu-law.

A. Esquema general del sistema

Diagram


"Entrada de Audio \n (Mic/Señal Analógica)" "ADC ESP32 \n (0-3.3V, 12 bits)" "m(u-law) \n Compresión 8 bits" "Transmisión Serial \n (UART + Clock externo)" "Recepción Serial \n (UART + Sync)" "m(u-law) \n Descompresión a 12 bits Inte.\n (interno DSP 12-16 bits en SW)" "DAC 8 bits \n (salida analógica)" "Filtro Pasabajos \n (RC o activo)" "Driver \n (Amplificador/Puente)" "Parlante \n (5 Ohm)"
1. Descripción de bloques
  • Entradas analógicas (0-3.3V): Se recomienda ajustar la fuente de señal o micrófono para no superar 3.3V y evitar saturar el ADC.
  • ADC del ESP32: Se configura con atenuación para maximizar la linealidad en el rango 0-3.3V (en el ESP32, la atenuación de 0 dB suele ser suficiente si cuidamos la ganancia anterior).
  • Compresión μ-law: Reducción de la resolución efectiva a 8 bits utilizando una curva logarítmica (μ=128).
  • Transmisión serial síncrona: Se utilizan pines UART TX, RX y se añade una línea extra para el clock. Aunque UART típicamente no usa clock externo, se puede generar una señal de sincronización para alinear muestreos o bytes.
  • Recepción y Descompresión: El segundo ESP32 recibe los bytes comprimidos, aplica la ley inversa y reconstruye la amplitud en un buffer interno (12 o 16 bits en software), para luego enviar a un DAC de 8 bits.
  • Filtro pasabajos: Suaviza los escalones del DAC y reconstruye la señal analógica (se dimensiona para frecuencias hasta ~4 kHz * 2.5 = 10 kHz de margen).
  • Driver de parlante: El ESP32 no puede entregar mucha corriente directamente; se recomienda un pequeño amplificador de audio o un transistor en configuración push-pull.

B. Adquisición y Compresión 1. ADC del ESP32
  • Resolución nominal: 12 bits.
  • Resolución efectiva: Alrededor de 10-11 bits típicamente, debido a no linealidad interna y ruido de conversión.
  • Rango de entrada recomendado: 0-3.3 V.
  • Atenuación: 0 dB para rango de 0-3.3 V (en algunos modelos de ESP32 se puede usar 2.5 dB o 6 dB según sea necesario).
  • Error de cuantización teórico:
    • Para un ADC ideal de 12 bits y rango ( 0 - 3.3 , \mathrm{V} ), el paso es ( \Delta = \frac{3.3\text{V}}{2^{12}} \approx 0.8 \text{ mV} ).
    • SQNR (Signal-to-Quantization Noise Ratio) ideal en dB para un convertidor de (n) bits:
      SQNR(dB)6.02n+1.76\mathrm{SQNR \, (dB)} \approx 6.02\,n + 1.76
      Para ( n=12 ), (\mathrm{SQNR} \approx 6.02\times12 + 1.76 \approx 73.0 ,\mathrm{dB}).
    • En la práctica, la linealidad del ESP32 reduce algo este valor.
2. Compresión μ-law (μ=128)
  • Ley μ en su forma continua:
    F(x)=sgn(x)ln ⁣(1+μx)ln(1+μ)F(x) = \operatorname{sgn}(x)\,\frac{\ln\!\bigl(1+\mu|x|\bigr)}{\ln(1+\mu)}
  • En la práctica, se discretiza a 8 bits: se genera una tabla de 256 valores para la compresión y otra para la descompresión en software.
  • Objetivo: Reducir el ruido perceptual en señales de voz y aprovechar mejor los pocos bits en la parte baja de la amplitud.

C. Transmisión digital UART + Sincronización
  1. Protocolo
    • Se define una trama básica: cada byte (8 bits) corresponde a la muestra comprimida μ-law.
    • Velocidad de baud recomendada (por ejemplo, 250 kBaud) con una línea adicional de reloj o "sync" a 16 kHz que marque el inicio de cada muestra.
    • Distancia entre microcontroladores = 5 cm: es muy corta, un simple par UTP con nivel TTL 3.3 V y quizás una resistencia pull-up en la línea de reloj basta.
  2. Sincronización
    • Puede implementarse un pin digital que cambie de estado cada vez que se envíe un nuevo byte comprimido.
    • El segundo MCU detecta ese flanco para alinear la lectura UART o para iniciar una interrupción.
  3. Conexión física (ejemplo de pines recomendados, ajustable según ESP32):
    • ESP32 #1 (Transmisor)
      • GPIO17 → TX UART
      • GPIO16 ← RX UART (no necesariamente usado si sólo emite).
      • GPIO4 → Clock de sincronización (pulso por byte).
      • GND común.
    • ESP32 #2 (Receptor)
      • GPIO17 ← RX UART
      • GPIO16 → TX UART (solo si se requiere respuesta)
      • GPIO5 ← Clock de sincronización
      • GND común.

D. Recepción, Descompresión y DAC
  1. Descompresión
    • Se utiliza la tabla inversa de µ-law. Cada byte (8 bits) se expande a 12 o 16 bits en software (p. ej., en variable uint16_t).
    • Se implementa un filtro digital de décimas si se quiere reducir ruido extra.
  2. DAC
    • El ESP32 carece de DAC de alta resolución en muchos modelos, típicamente 8 bits en dos canales (GPIO25 y GPIO26).
    • Se escribe la amplitud reconstruida (recortada a 8 bits).
    • Se filtra de manera analógica para suavizar la señal escalonada.
  3. Driver de parlante
    • Se recomienda un pequeño amplificador en puente (ej. una etapa push-pull con transistores o un amplificador en clase D si se quiere más eficiencia).
    • Parlante de 5 Ω requiere un control de corriente mayor de lo que el pin DAC puede entregar directamente.

E. Aspectos temporales y de muestreo
  1. Frecuencia máxima de señal = 4 kHz
    • Según Nyquist, la frecuencia de muestreo mínima (f_s) debe ser > 8 kHz.
    • Para mayor margen y calidad, se podría muestrear a 16 kHz.
    • Con compresión a 8 bits, la tasa de bits es 16 kS/s * 8 bits = 128 kbps.
  2. Diagrama de tiempos (simplificado)
    1. Cada (\Delta t = 1 / f_s), generar interrupción de timer.
    2. Leer ADC (12 bits) → Comprimir a 8 bits.
    3. Transmitir 1 byte por UART.
    4. Generar pulso en señal de sincronización.
    5. El receptor detecta el pulso, lee el byte UART → Descomprime → Envía al DAC.

F. Ejemplo de Código (Arduino) – Transmisor
Nota: Este código es simplemente ilustrativo y puede requerir ajustes a la librería o a la implementación concreta de mu-law. Las funciones de compresión y la tabla deben crearse según la curva μ-law.

Cpp


/* Ejemplo Transmisor PCM con μ-law (ESP32) */

#include <driver/adc.h>
#include <HardwareSerial.h>

#define FS 16000       // Frecuencia de muestreo deseada
#define SYNC_PIN 4     // Pin de sincronización (salida)
#define ADC_CHANNEL ADC1_CHANNEL_0 // GPIO36 en algunos ESP32

HardwareSerial SerialPCM(2); // UART2

// Tabla de compresión mu-law (256 valores)
uint8_t muLawCompressTable[4096]; // Ajustar según max. 12 bits si se desea
// (Se genera offline o con una rutina setupMukawTables())

hw_timer_t *timer = NULL;

// Rutina de interrupción de muestreo
void IRAM_ATTR onTimer() {
  // 1. Leer ADC (12 bits)
  int raw = adc1_get_raw(ADC_CHANNEL); // 0-4095 (aprox. con atenuación=0)
  // 2. Comprimir a 8 bits mu-law
  //   Suponiendo que "raw" se normaliza de 0 a 4095, se centra en +/-2048, etc.
  //   Ejemplo pseudo-línea:
  uint8_t muValue = muLawCompressTable[ raw & 0xFFF ]; 

  // 3. Generar pulso de sincronización
  digitalWrite(SYNC_PIN, HIGH);

  // 4. Enviar el byte por UART
  SerialPCM.write(muValue);

  // 5. Bajar pulso
  digitalWrite(SYNC_PIN, LOW);
}

void setup() {
  // Config ADC
  adc1_config_width(ADC_WIDTH_BIT_12);
  adc1_config_channel_atten(ADC_CHANNEL, ADC_ATTEN_DB_0);

  // Config pin de sincronización
  pinMode(SYNC_PIN, OUTPUT);
  digitalWrite(SYNC_PIN, LOW);

  // Config UART
  SerialPCM.begin(250000, SERIAL_8N1, -1, -1, false); // TX default pin GPIO17
  
  // (Inicializa tabla muLawCompressTable) => setupMuLawTables();

  // Config timer a FS = 16 kHz
  timer = timerBegin(0, 80, true); 
  // A 80 MHz / 80 = 1 MHz -> se cuentan 1 microseg = 1 tick
  // Queremos 16000 Hz => 1/16000 = 62.5us, => 62.5 ticks
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 62, true);
  timerAlarmEnable(timer);
}

void loop() {
  // Hacer otras tareas
}
Comentarios principales:
  • Se deja la muestra en 12 bits, se usa la tabla muLawCompressTable[] para obtener el valor 8 bits.
  • La línea de sincronización (GPIO4) genera un pulso cada vez que se lee/senvía una muestra.
  • El baudrate de 250000 es un ejemplo; se debe ajustar para que en ~62 µs se alcance a enviar 1 byte (1 start bit + 8 data + 1 stop = 10 bits → 10/250000=40 µs aprox.). Hay un margen de ~22 µs para overhead.

G. Ejemplo de Código (Arduino) – Receptor

Cpp


/* Ejemplo Receptor PCM con μ-law (ESP32) */

#include <driver/dac.h>
#include <HardwareSerial.h>

#define FS 16000
#define SYNC_PIN 5        // Pin de entrada de sincronización
#define DAC_CHANNEL DAC_CHANNEL_1 // DAC: GPIO25 en ESP32

HardwareSerial SerialPCM(2); // UART2
hw_timer_t *timer = NULL;
volatile uint8_t lastSample = 0;

// Tabla inversa (para 8 bits a 12 o 16 bits) – de preferencia 256 valores
uint16_t muLawDecompressTable[256];

void IRAM_ATTR onSync() {
  // Leer el byte disponible en UART (blocking o buffer)
  if (SerialPCM.available()) {
    lastSample = SerialPCM.read();
  }
  // Alternativa: se podría alinear la interrupción con un timer si queremos exactitud 
  // de muestreo local. Depende del diseño final.
}

void IRAM_ATTR onTimer() {
  // Reconstruir muestra 12 bits
  uint16_t expanded = muLawDecompressTable[lastSample]; // 0..4095 (~12 bits)
  
  // Reducir a 8 bits para DAC
  uint8_t dacValue = expanded >> 4; // descartar 4 bits
  dacWrite(DAC_CHANNEL_1, dacValue);
}

void setup() {
  // Config DAC
  dacOutputEnable(DAC_CHANNEL_1);

  // Config UART
  SerialPCM.begin(250000, SERIAL_8N1, -1, -1, true); 
  // "true" => Invertir pines si es necesario. Ajustar según tu ESP32.

  // Config pin de sincronización
  pinMode(SYNC_PIN, INPUT_PULLDOWN);
  attachInterrupt(digitalPinToInterrupt(SYNC_PIN), onSync, RISING);

  // (Inicializa tabla muLawDecompressTable)

  // Config timer 
  // Nota: si se quiere un playback sin dependencia exacta del pin SYNC, 
  // se programa el timer a 16 kHz y en la callback onTimer() se lee "lastSample". 
  // O se puede directamente emplear la interrupción onSync para doEverything. 
  timer = timerBegin(1, 80, true);
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 62, true);
  timerAlarmEnable(timer);
}

void loop() {
  // Otras tareas
}
Comentarios:
  • Se asume que llega un byte cada ~62 µs.
  • lastSample se actualiza de forma asíncrona.
  • onTimer() genera la salida de audio al DAC con la última muestra recibida.
  • Se puede combinar la lectura y la reproducción en la misma ISR si se desea sincronización estricta con el pulso.

H. Protección y Filtrado
  1. Protección contra sobrevoltaje
    • Para la entrada ADC: un diodo de clamp + resistencia en serie (p.ej., 10 kΩ) para evitar picos > 3.3V.
  2. Filtros analógicos
    • Antialias en la entrada del ADC: un filtro RC pasa-bajos con (f_c \approx 5\text{ kHz}) si la señal de interés es hasta 4 kHz.
    • Reconstrucción en la salida DAC: un filtro RC o activo (op-amp) con (f_c) algo mayor a 4 kHz (p.ej., 8-10 kHz).

I. Cálculos de la Frecuencia de Muestreo y Tasa de datos
  • Muestreo: (f_s = 16\text{ kHz}).
  • Bits por muestra: 8 bits (gracias a la compresión μ-law).
  • Tasa de datos: (16,000 \times 8 = 128 \text{ kbps}).
  • Tiempo por byte: (1/16,000 \approx 62.5,\mu s).

J. Relación Señal/Ruido de Cuantización (SQNR) y Distorsión μ-law
  1. SQNR con compresión
    • Para 8 bits lineales típicos: (\mathrm{SQNR} \approx 6.02 \times 8 + 1.76 = 49.9,\mathrm{dB}).
    • Con μ-law y señal de voz, la calidad percibida supera la simple cuantización lineal de 8 bits, especialmente en señales de bajo nivel.
  2. Distorsión por compresión
    • La compresión introduce mayor error en amplitudes altas debido a la curva logarítmica; no obstante, aporta mejor relación señal/ruido en amplitudes bajas.
    • Normalmente aceptable para voz o señales de ancho de banda reducida (~300 Hz a 4 kHz).

K. Tabla de Parámetros Ajustables

Table


ParámetroValor/RecomendaciónObservaciones
Frecuencia de Muestreo (fs)16 kHzAsegura ancho de banda hasta ~8 kHz (sobrado para voz a 4 kHz).
Atenuación ADC ESP320 dB (por defecto)Ajustar si la señal de entrada excede 3.3V.
Bits finales (compresión)8μ-law.
μ (Factor de compresión)128Ajustable: 255 suele ser tambien frecuente.
Baud Rate UART250 kBaud (mín)Ajustar para no superar el tiempo de muestreo.
Filtro Antialias (RC)~5 kHzDepende de la señal de entrada.
Filtro Reconstrucción (RC)~8-10 kHzPara suavizar la salida a 8 bits.
Pin SYNC adicional1 señal digitalAlinea muestras; se puede omitir con buffers FIFO.
Amplificador de salidaTransistor push-pull o Amp audio bajo consumoPara parlante de 5 Ω.

L. Conclusiones y Siguientes Pasos
  1. Integración final: Verificar que el timing sea estable. Usar osciloscopio para comprobar la señal ADC, la trama UART y el pulso de sincronía.
  2. Ruido y distorsión: Ajustar filtro pasabajos y realizar pruebas de escucha con el parlante.
  3. Optimizaciones:
    • Ajustar interrupciones y prioridad para no introducir jitter.
    • Usar buffers circulares (colas) y DMA en UART si se requiere robustez.
    • Usar un amplificador en la salida para el parlante (p. ej., un pequeño módulo clase D de 2-3 W).
Con esto se logra un sistema de transmisión PCM comprimida (μ-law) a baja distancia (~5 cm) entre dos ESP32, capturando hasta 4 kHz de audio y reproduciéndolo en el otro extremo. Esto cubre tanto los cálculos de muestreo, la comunicación serial, la compresión logarítmica y la reconstrucción de señal con DAC + filtro.
¡Éxitos en tu proyecto!
Define project properties
Search for main components
Add ADC and DAC components
"module ALU\_N\_bits #( parameter N = 8 )( input wire \[N-1:0] A, input wire \...

@copilot "module ALU_N_bits #( parameter N = 8 )( input wire [N-1:0] A, input wire [N-1:0] B, input wire [2:0] control, // Bus de control para seleccionar la operación output wire [N-1:0] result, output wire carry_borrow );
wire [N-1:0] sum_res, and_res, or_res, xor_res, not_res, shl_res, shr_res;
wire sum_carry;

// Instanciamos los módulos de operaciones
add_sub #(.N(N)) add_sub_inst (
    .A(A),
    .B(B),
    .sel(control[0]), // Si control[0] = 0 -> Suma, si es 1 -> Resta
    .R(sum_res),
    .carry_borrow(sum_carry)
);

and_nbits #(.N(N)) and_inst (
    .A(A),
    .B(B),
    .Y(and_res)
);

or_nbits #(.N(N)) or_inst (
    .A(A),
    .B(B),
    .Y(or_res)
);

xor_nbits #(.N(N)) xor_inst (
    .A(A),
    .B(B),
    .Y(xor_res)
);

not_nbits #(.N(N)) not_inst (
    .A(A),
    .Y(not_res)
);

shift_left_nbits #(.N(N)) shl_inst (
    .A(A),
    .Y(shl_res)
);

shift_right_nbits #(.N(N)) shr_inst (
    .A(A),
    .Y(shr_res)
);

// Multiplexor para seleccionar la operación
assign result = (control == 3'b000) ? sum_res :    // Suma o Resta
                (control == 3'b001) ? and_res :    // AND
                (control == 3'b010) ? or_res  :    // OR
                (control == 3'b011) ? xor_res :    // XOR
                (control == 3'b100) ? not_res :    // NOT (Solo A)
                (control == 3'b101) ? shl_res :    // Shift Left
                (control == 3'b110) ? shr_res :    // Shift Right
                {N{1'b0}}; // Default: 0

// El carry/borrow solo tiene sentido en suma/resta
assign carry_borrow = (control == 3'b000) ? sum_carry : 1'b0;
endmodule "
Puedes hacer una construcción donde muestren cómo los módulos están interconectados. Además, realiza una breve explicación de cómo funciona y las consideraciones de diseño e implementación que tendrías en cuenta si fueras a diseñar el módulo.
1y
Let's create a diagram to illustrate how the modules in the ALU are interconnected and provide a brief explanation of how the ALU functions, along with some design and implementation considerations.
Interconnection Diagram
Here is a simplified block diagram of the ALU_N_bits module, showing the interconnections between the various operation modules and the multiplexer used to select the output based on the control signal:
graph TD;
    A[Input A $N-1:0$] --> add_sub_inst;
    B[Input B $N-1:0$] --> add_sub_inst;
    add_sub_inst -->|Sum Result| sum_res;
    add_sub_inst -->|Carry| sum_carry;

    A --> and_inst;
    B --> and_inst;
    and_inst --> |AND Result| and_res;

    A --> or_inst;
    B --> or_inst;
    or_inst --> |OR Result| or_res;

    A --> xor_inst;
    B --> xor_inst;
    xor_inst --> |XOR Result| xor_res;

    A --> not_inst;
    not_inst --> |NOT Result| not_res;

    A --> shl_inst;
    shl_inst --> |Shift Left Result| shl_res;

    A --> shr_inst;
    shr_inst --> |Shift Right Result| shr_res;

    control[Control $2:0$] --> mux;
    sum_res --> mux;
    and_res --> mux;
    or_res --> mux;
    xor_res --> mux;
    not_res --> mux;
    shl_res --> mux;
    shr_res --> mux;
    mux --> |"Result $N-1:0$"| result;

    sum_carry --> |Carry/Borrow| carry_borrow;
    control --> carry_borrow;
Functionality and Explanation
  1. Inputs and Outputs:
    • The ALU takes two N-bit wide inputs, A and B, and a 3-bit control input to determine the operation to perform.
    • It produces an N-bit wide result and a carry/borrow flag for arithmetic operations (add/subtract).
  2. Operation Modules:
    • The add_sub module performs addition or subtraction between A and B based on the least significant bit of the control signal, control[0].
    • Logical operations include AND, OR, and XOR, handled by corresponding modules.
    • The not_nbits module complements the bits of A.
    • Shift operations (left and right) manipulate the bit positions of A.
  3. Multiplexer (MUX):
    • A multiplexer selects which operation's result to output based on the control signal. Each control pattern corresponds to a different operation:
      • 3'b000: Addition or Subtraction
      • 3'b001: AND
      • 3'b010: OR
      • 3'b011: XOR
      • 3'b100: NOT
      • 3'b101: Shift Left
      • 3'b110: Shift Right
  4. Carry/Borrow:
    • The carry/borrow is only relevant for addition or subtraction, so it is passed through only when the control is for the sum operation (3'b000).
Design and Implementation Considerations
  • Parameterizability: The use of a parameter $N$ makes the module scalable for different bit-width operations.
  • Modularity: Each operation is modularized, allowing easy updates or enhancements without affecting the entire ALU.
  • Default Output: When the control signal does not match an operation, the ALU outputs a default value of zero.
  • Timing: Consideration for timing constraints, especially for larger bit-widths, as some operations might become timing-critical.
  • Resource Usage: Balance between resource usage (e.g., LUTs in FPGAs) and speed, particularly in the context of target technology (ASIC vs. FPGA).
  • Testability: Ensure thorough testing for each individual module as well as the overall ALU to verify correct functionality under various conditions.
1y
1
A
K

No files yet

Documents

    Documents are markdown text files which describe project goals, capture details, or even simulation outputs.

    Assets

    led-on.svg

    led-on

    LED_1206_3216Metric.kicad_mod

    LED_1206_3216MetricFootprint
    LED_1206_3216Metric.step

    LED_1206_3216Metric.step

    3D_LED_1206_3216Metric3D Model

    led-off.svg

    led-offDefault

    [Example] LED Simulator Template 529a

    [Example] LED Simulator Template 529a thumbnail
    Simulator template for an LED part. Check out the Code editor for instructions!

    Properties

    1.70

    V

    LED

    CTL1206FYW1T

    LED

    Pricing & Availability

    DPN

    Stock

    Qty 1

    1

    21K

    $0.03655

    1

    0

    $0.113

    Controls