Analizador de audio profesional

Ratmayor

ModeraTroll
Este es un proyecto extremadamente ambicioso que llevo dándole vueltas en la cabeza un buen rato, pero esta incompleto, así que iré subiendo los avances paulatinamente y son bienvenidos a dar sus sugerencias y opiniones.

Se trata de un equipo que permite medir distorsión armónica (THD), relación señal / ruido (SNR), distorsión / ruido (THD+N), distorsión por intermodulación (IMD), potencia, damping factor, respuesta de frecuencias, parámetros T/S entre otras cosas que pueda hacer con el mismo hardware y me vaya acordando 🤣🤣🤣

De momento iré compartiendo los diagramas y a medida que vaya investigando y diseñando iré explicando una a una las etapas y su funcionamiento, antes de pelearme con el software (Si, requiere software porque estaré usando 2 MCU)

Adjunto los diagramas de los datasheets de alguno de los ICs que usaré en el proyecto...

Etapa de entrada
Bien comencemos desde el principio el equipo cuenta con varios puntos de prueba, el primero es para conectar la fuente de sonido que se quiera probar, ya sea un amplificador o preamplificador, cuenta con 3 escalas seleccionables por medio de relés de 2.5V, 25V y 250V por lo que los divisores de tensión están calculados para el tope de escala del ADC (2.5V). En la entrada principal hay varias derivaciones que van a diferentes puntos del circuito, la linea marcada como ADC1 va para el ADC CS5389 que sera para mediciones de alta precisión como la THD, IMD, Slew Rate, etc. La línea marcada como IPR va con un sensor ACS712 para medir la corriente que circula hacia las cargas en el caso de estar probando la potencia de un amplificador, así como también la línea marcada como VPR es para medir la tensión que está ingresando, así podemos calcular la potencia y el damping factor, ya la última línea marcada como ARG va directamente al MCU de control y es para leer en que escala estamos, nos sirve como referencia para que el sistema sea auto rango y también como medida de protección en caso de exceder la escala seleccionada.

También hay un terminal marcado como Speaker que es para realizar pruebas de parámetros T/S, la línea marcada como Amp viene desde un pequeño amplificador interno basado en el LM1875 que tiene suficiente potencia para excitar el parlante y calcular su masa, para estas pruebas se usan simultáneamente los 2 ADCs internos del CS5389.

Ya por último cuenta con 2 entradas extra en donde se conectará la alimentación del amplificador que se esté probando y así poder calcular que tanta señal se le puede inyectar sin que se sature y poder calcular la eficiencia.
front-end.png
También cabe destacar que la selección de las escalas y funciones se harán por medio de pequeños relés DIP para no comprometer las mediciones, excepto los relés que activan / desactivan las cargas, esos si son relés de machos 🤣

Etapa de conversión analógica / digital
El ADC escogido es un CS5389 de 18bits, que además tiene una baja relación señal ruido de 100dB y un rango dinámico de 107dB, además entre los ADC exquisitos de encapsulado DIP (Sip, todo el proyecto es thru-hole, ya mi vista no es la de hace 20 años atrás) es el menos caro, claro que hay mejores opciones, pero estamos hablando de us$70 o más 🫠 y este se mantiene en el orden de los us$15 es un lujo, pero es manejable.
buffer_adc.png
El ADC requiere de señales balanceadas y para lograr esto, se escogió los OPA2132 ya que cuenta con la locura de 0.00008% de distorsión además de un ancho de banda de 8Mhz, lo que lo deja bastante sobrado para este proyecto. Las secciones como las de medición de potencia, damping factor, etc al no necesitar una precisión extrema, usan directamente los ADC del dsPIC30F4013I/P.

Es importante recalcar que se requiere el uso de resistores de metal film al 1% o al 0.1% para mantener la precisión del equipo, además que el sistema requiere de diferentes fuentes de alimentación que deben estar separadas, por eso se marcan como ±5VD y ±5VA en referencia a 5V digitales y 5V analógicos, incluso las conexiones a tierra también deben ir separadas y eso es para evitar que los ruidos de los clock y data se cuelen a las líneas analógicas, lo que afectaría gravemente las mediciones. No obstante, si podemos mantener el ruido al mínimo, el equipo tiene la sensibilidad de detectar niveles de distorsión del 0.001% que para ser un proyecto DIY es bastante generoso...
 

Adjuntos

  • CS5389.pdf
    1.7 MB · Visitas: 12
  • opa2132.pdf
    1.3 MB · Visitas: 2
  • DAC.pdf
    37.3 KB · Visitas: 0
  • CPU.pdf
    59.2 KB · Visitas: 2
  • CS4328.pdf
    951.2 KB · Visitas: 0
  • ADC.pdf
    62.1 KB · Visitas: 1
  • DSP.pdf
    42.1 KB · Visitas: 2
DSP, el musculo matemático
Para este proyecto escogí el dsPIC30F4013 qué es un controlador digital de señales (DSC) de alto rendimiento de 16 bits, que combina la potencia de cálculo de un procesador de señales digitales (DSP) con la facilidad de uso de un microcontrolador tradicional y además tengo varios ahí guardados :V

Al principio pensé en usar un solo DCP para todo, para los cálculos necesarios y para la interfaz de usuario, PERO, este MCU solo cuenta con 1kb de ROM y 2kb de RAM lo que se iba a convertir en un problema a la hora de calcular la FFT o correr los algoritmos Goertzel qué calculan la distorsión armónica si además tenía que atender los botones y la LCD 🤦🏻‍♂️

Bueno, vamo' a calmano', primero veamos la distribución del dsPIC30F4013, se está aprovechando el hardware del DSP, eso de andar haciendo bit-banging me da ansiedad 🤣 por lo que los pines quedaron asignados así:
Pinassing.png
Así sacamos el máximo provecho de los chips y reducimos la latencia.

Resolviendo problemas y limitaciones
Como comentaba el dsPIC30F4013 tiene una ROM de 1Kb y una RAM de 2Kb lo que para los procesos matemáticos pudiera verse limitado, así que se agregaron 2 memorias externas, una RAM 23LC1024 de 128kb y una EEPROM 24FC1025 para guardar datos de calibración y otros parámetros, también el cristal está calculado para que trabaje a ritmo con el ADC y el DAC para obtener una frecuencia de muestreo de 48kHz trabajando en el puerto DCI o Interfaz de Convertidor de Datos (DCI por sus siglas en inglés) que le permiten al dsPIC la interconexión sencilla de dispositivos, como codificadores/decodificadores de audio (códecs), convertidores A/D y convertidores D/A. El DCI gestiona automáticamente la
temporización de la interfaz asociada a estos códecs. No se requiere ninguna sobrecarga de la CPU hasta que la cantidad de datos solicitada se haya transmitido o recibido por el DCI. Se pueden transferir hasta cuatro palabras de datos entre interrupciones de la CPU, lo que vienen bien para dedicar más potencia de cálculo a los algoritmos...
DSP.png
La memoria RAM va conectada directamente en el puerto SPI nativo del MCU, la ROM va en el puerto nativo I2C y la comunicación con el otro MCU será vía UART.

El MCU recibe directamente las señales de prueba de tensión de fuente y potencia ya que como mencioné antes, no se requiere de demasiada precisión para estas mediciones, además, como me estresa ver un MCU con demasiados pines libres, se le agregaron 3 LEDs:, Blanco: MCU OK, Verde: MCU Busy, Rojo: MCU Error y también un pequeño buzzer para el bip de los botones...
 
Última edición:
Luego de pasar una virosis con todo y fibre prosigo con el proyecto :'V esta vez analicemos al director de la orquesta:

Unidad Control e Interfaz de usuario
Esta etapa está trabajando bajo el mando del PIC18F4550, ideal por tener un buen clock PLL 48Mhz, lo que le dará fluidez a la interfaz, este trabajará solo para que el usuario pueda comunicarse con el DSP (dsPIC30F4013) quien es quien realmente hace el trabajo matemático pesado y llama las funciones que hemos agregado al sistema que son:
  • Medición THD, THD+N, IMD, SINAD.
  • Ancho de banda (Barrido de 10Hz a 30kHz).
  • Analizador de espectro (FFT, lectura básica algo limitada por el dsPIC y la LCD que es de 20x4, pero podría ser de ayuda).
  • Medición de potencia RMS, Peak y DIN.
  • Medición de Factor de amortiguamiento (Damping Factor).
  • Medición de Slew-Rate.
  • Medición de parámetros T/S para parlantes.
  • Generador de frecuencia senoidal y cuadrada (10Hz a 30kHz).
  • Selector de cargas 4Ω y 8Ω
Teniendo en cuenta que el PIC18F4550 tiene puerto USB consideré que a futuro conforme reciba ayuda para el desarrollo de este equipo se pueda usar para transferir todos los datos recogidos a una PC y generar reportes.

CPU.png
Además del PIC, le agregué un desplazador de registro 74LS164 para activar / desactivar los LEDs que acompañan los botones, además de un rotary encoder para navegar entre las opciones de cada una de las pruebas. Además este MCU se encargará también de detectar el nivel de tensión que está entrando al equipo a modo de protección y para el auto rango (Pin ARG) y un pequeño LM35 para supervisar la temperatura de las resistencias de carga, además de controlar los relés que seleccionan las funciones y los rangos de la entrada (RY*) y mantiene comunicación constante con el DSP vía UART (U2TX, U2RX)

Conversión Digital - Analógica, Salidas
Esta es la etapa que más llevó revisiones, en especial porque hay pocos DAC "Exquisitos" de encapsulado DIP por los cuales no te quieran pedir un riñón, lo intenté con el AD1865, TDA1540, TDA1541, DAC745KP y el PCM1702 hasta que me topé con el DS4328 que cuenta con 18bits al igual que ADC, lo que me evitaba que no se entendiera con el ADC y que el dsPIC tenga que hacer de traductor, además de un muy bajo nivel de distorsión y ruido, además a un precio módico
DAC.png
El DAC, al ser estéreo, uso los 2 canales para funciones diferentes, el canal L o 1, es la salida de frecuencia que además servirá para enviar señales limpias al amplificador que tengamos en prueba, y el canal R o 2 lo uso exclusivamente para el LM1875 que a su vez servirá de excitador para el parlante que tengamos a prueba en el modo de prueba de parámetros T/S.

De momento esto es lo que llevo desarrollado, espero encarecidamente me puedan apoyar en el desarrollo de este proyecto y por mi parte mientras ahorro para comprar algunos ICs que me faltan y voy diseñando la PC, ir compartiendo lo que vaya programando...

Saludos a todos...
 
Última edición:
Luego de pasar una virosis con todo y fibre prosigo con el proyecto :'V esta vez analicemos al director de la orquesta:

Unidad Control e Interfaz de usuario
Esta etapa está trabajando bajo el mando del PIC18F4550, ideal por tener un buen clock PLL 48Mhz, lo que le dará fluidez a la interfaz, este trabajará solo para que el usuario pueda comunicarse con el DSP (dsPIC30F4013) quien es quien realmente hace el trabajo matemático pesado y llama las funciones que hemos agregado al sistema que son:
  • Medición THD, THD+N, IMD, SINAD.
  • Ancho de banda (Barrido de 10Hz a 30kHz).
  • Analizador de espectro (FFT, lectura básica algo limitada por el dsPIC y la LCD que es de 20x4, pero podría ser de ayuda).
  • Medición de potencia RMS, Peak y DIN.
  • Medición de Factor de amortiguamiento (Damping Factor).
  • Medición de Slew-Rate.
  • Medición de parámetros T/S para parlantes.
  • Generador de frecuencia senoidal y cuadrada (10Hz a 30kHz).
  • Selector de cargas 4Ω y 8Ω
Teniendo en cuenta que el PIC18F4550 tiene puerto USB consideré que a futuro conforme reciba ayuda para el desarrollo de este equipo se pueda usar para transferir todos los datos recogidos a una PC y generar reportes.

Ver el archivo adjunto 339938
Además del PIC, le agregué un desplazador de registro 74LS164 para activar / desactivar los LEDs que acompañan los botones, además de un rotary encoder para navegar entre las opciones de cada una de las pruebas. Además este MCU se encargará también de detectar el nivel de tensión que está entrando al equipo a modo de protección y para el auto rango (Pin ARG) y un pequeño LM35 para supervisar la temperatura de las resistencias de carga, además de controlar los relés que seleccionan las funciones y los rangos de la entrada (RY*) y mantiene comunicación constante con el DSP vía UART (U2TX, U2RX)

Conversión Digital - Analógica, Salidas
Esta es la etapa que más llevó revisiones, en especial porque hay pocos DAC "Exquisitos" de encapsulado DIP por los cuales no te quieran pedir un riñón, lo intenté con el AD1865, TDA1540, TDA1541, DAC745KP y el PCM1702 hasta que me topé con el DS4328 que cuenta con 18bits al igual que ADC, lo que me evitaba que no se entendiera con el ADC y que el dsPIC tenga que hacer de traductor, además de un muy bajo nivel de distorsión y ruido, además a un precio módico
Ver el archivo adjunto 339939
El DAC, al ser estéreo, uso los 2 canales para funciones diferentes, el canal L o 1, es la salida de frecuencia que además servirá para enviar señales limpias al amplificador que tengamos en prueba, y el canal R o 2 lo uso exclusivamente para el LM1875 que a su vez servirá de excitador para el parlante que tengamos a prueba en el modo de prueba de parámetros T/S.

De momento esto es lo que llevo desarrollado, espero encarecidamente me puedan apoyar en el desarrollo de este proyecto y por mi parte mientras ahorro para comprar algunos ICs que me faltan y voy diseñando la PC, ir compartiendo lo que vaya programando...

Saludos a todos...
El XLR será Out ¿no?
 
El XLR será Out ¿no?
Sí señor, sino perdería la profesionalidad, el buffer balanceado está conformado por un OpAmp cuádruple OPA4132
Ver el archivo adjunto 339939
He aquí el código del dsPIC30F4013, lo generé con IA y no, no me da vergüenza admitirlo, requiere depuración, pero el sueño me venció, mañana seguimos... :cool:

Código movido a archivos adjuntos debido a que el foro no soporta tantos caracteres

- Revisión 1.0: La IA no tenía ni idea de que programo en CCS PIC-C.
- Revisión 1.1: La IA seguía sin entender que estamos hablando en CCS PIC y escribió cosas raras.
- Revisión 1.2: La IA ya se enteró de que estamos en CCS PIC-C pero generó un código ambiguo
- Revisión 1.3: La IA escribió un código desordenado y me seguía llevándo la contraria
- Revisión 1.4: La IA me había dejado los ADC / DAC de 18bits limitados a 16bits 🤦🏻‍♂️
- Revisión 1.5: La IA corrigió el uso de los 18bits de los ADC / DAC, pero olvidó el hecho de que tenemos una RAM externa y la compilación quedaba desbordada.
- Revisión 1.6: La IA No sabía como hablar con el puerto DCI del dsPIC, tuve que darle las instrucciones internas del driver 30F4013.h
- Revisión 1.7: La IA ya sabía como hablarle al puerto DCI, pero dejaba detalles por fuera, como el sample rate.
- Revisión 1.8: Bueno, al menos compiló sin errores 🤣 pero hasta a mi se me olvidó integrar la EEPROM que se supone es para datos de calibración 🤦🏻‍♂️
- Revisión 1.9: En teoría ahora si está bien, solo falta que me lleguen los ADC / DAC para comenzar a hacer pruebas...
- Revisión 2.0: Se hicieron varios cambios porque no me puedo quedar tranquilo, el primero de ellos fue optimizar el uso de la RAM, en lugar de enviar estructuras definidas por cada función, uso otra función dinámica que cree la estructura que envíe y reciba los datos, el segundo cambio es que ahora el CPU tiene acceso a la EEPROM, de manera que la EEPROM ya no es solo para parámetros de calibración interna, sino para establecer parámetros de medición y que estos queden guardados en la memoria, tercero, se reagruparon las funciones, de manera que ahora puedo activar varias pruebas a la vez y se irán realizando secuencialmente.
- Revisión 2.1: Se añadieron algunas funciones que habían quedado olvidadas y se integraron unas nuevas: Generador de frecuencia, ya está incluido en el código, La función para medir parámetros T/S ya se encuentra en el código, La función para medir potencia RMS, Peak y DIN, ahora también aprovecha que el dsPIC lee las tensiones de alimentación del amp de prueba y calcula el clipping, el offset y la eficiencia, la función Sweep que comprueba el ancho de banda ahora también mide el Noise Floor y el rango dinámico, se añadió un menú de servicio para calibrar los ADC, se comentó el código a nivel for dummies para que fuese más digerible, se corrigieron algunos bugs y se agregaron otros más para corregirlos más tarde...
- Revisión 2.2: Generador mejorado con seno/cuadrada/triangular, interpolación lineal, ganancia ajustable en tiempo real, se corrigieron los módulos RunTHD y RunTS que no tenían acceso al DAC para generar las ondas para sus respectivas pruebas.
- Revisión 2.3: Había funciones huérfanas y variables apuntando a nada, además, aunque el puerto DCI tenía prioridad en el sistema por medio del hardware, seguía corriendo por software mediante polling.
- Revisión 2.4: Se optimizaron las funciones para aprovechar el puerto DCI por interrupciones del MCU, además se limpió todo lo que sobraba y se unificaron varias funciones para organizarlas por categorías. Además el generador DDS ahora funciona por interrupciones del MCU lo que lo hace independiente de los demás procesos, además se limpiaron las tablas para formar las ondas más dinámicas, lo que ahora permite generar ondas arbitrarias. La comunicación UART se optimizó tanto que incluso podría comunicarse con una PC y un script Phyton.
- Revisión 2.5: Me acordé de que tengo por ahí unos MAX264 (filtros programables de 4th orden) lo que da cabida a correr 2 tipos de algoritmos de detección de distorsión por lo que se agregaron nuevas funciones.
- Revisión 2.6: Se rompió todo, el modo "Residual" y el modo "Goertzel" corrían al mismo tiempo tragando RAM como si no hubiese un mañana, además el PIC18 no tenía ni idea de como decirle al dsPIC como cambiar el modo de análisis de distorsión.
- Revisión 2.7: Se logró unificar los algoritmos sin que se rompiera nada, al menos en teoría... Ya se puede hablar directamente al dsPIC para ingresar parámetros, tipo de pruebas y sus variaciones, además de la posibilidad de correr un autotest.
- Revisión 2.8: Me había olvidado por completo de implementar la detección de distorsión por intermodulación 🤦🏻‍♂️
- Revisión 2.9: Aunque hay nuevas funciones implementadas, el sistema quedó tragando RAM como si no hubiese un mañana
- Revisión 3.0: Se optimizaron los procesos y el consumo de RAM bajó considerablemente, además de integrar todos los tipos de distorsión a una sola función.


Nota importante, tanto los archivos como el código cambian sin previo aviso para mejorar u ocultar evidencia de metidas de pata, se debe estar al pendiente con la revisión 😁
 

Adjuntos

  • DSP.zip
    11.7 KB · Visitas: 1
Última edición:
Sí señor, sino perdería la profesionalidad, el buffer balanceado está conformado por un OpAmp cuádruple OPA4132
Ver el archivo adjunto 339939
He aquí el código del dsPIC30F4013, lo generé con IA y no, no me da vergüenza admitirlo, requiere depuración, pero el sueño me venció, mañana seguimos... :cool:
C:
/* ==========================================================================
 * RatNET Audio Analyzer - Firmware DSP (dsPIC30F4013)
 * Autor: Carlos Alvarez
 * Rev. 1.9
 * ========================================================================== */

#include <30F4013.h>
#include <math.h>
#include <string.h>

// ==========================================
// 1. FUSES Y CONFIGURACIÓN DEL RELOJ
// ==========================================
#FUSES NOWDT, HS, NOPROTECT, NODEBUG, NOWRT, NOBROWNOUT
#FUSES XT_PLL4
#use delay(clock=49152000)

// ==========================================
// 2. DEFINICIONES DE HARDWARE, PINES Y CONSTANTES DCI
// ==========================================
#define LED_OK      PIN_B5
#define LED_BUSY    PIN_B6
#define LED_ERROR   PIN_B7
#define BUZZER      PIN_B8
#define SRAM_CS     PIN_D1

// Comandos UART
#define CMD_SET_MODO_THD      0x10
#define CMD_SET_MODO_BAND     0x11
#define CMD_SET_MODO_ESPECTRO 0x12
#define CMD_SET_MODO_VU       0x13
#define CMD_POTENCIAS         0x30
#define CMD_Damping           0x40
#define CMD_TS_PARAMS         0x50
#define CMD_SLEW_RATE         0x60
#define CMD_GENERADOR         0x70
#define CMD_START             0x20
#define CMD_STOP              0x21
#define CMD_SERVICIO_LECTURA  0x80
#define CMD_SERVICIO_TEST_DAC 0x81
#define CMD_GUARDAR_CALIBRACION 0x90

#define ACK                   0x06
#define NACK                  0x15

// ==========================================
// 3. COMUNICACIÓN (UART, SPI para SRAM, I2C para EEPROM)
// ==========================================
#use rs232(UART2, baud=9600, xmit=PIN_F4, rcv=PIN_F5, stream=UART_PIC18, ERRORS)

// SPI2 Hardware para la SRAM 23LC1024
#use spi(MASTER, CLK=PIN_F6, DI=PIN_F2, DO=PIN_F3, BAUD=10000000, stream=SPI_SRAM)

// I2C por Software para la EEPROM 24FC1025 (RF0=SDA, RF1=SCL)
#use i2c(MASTER, SDA=PIN_F0, SCL=PIN_F1, FAST=400000, stream=I2C_EEPROM)

// ==========================================
// 4. ESTRUCTURAS DE DATOS Y VARIABLES GLOBALES
// ==========================================
typedef struct {
    float thdPorcentaje;
    float thdNPorcentaje;
    float imdPorcentaje;
    float sinadDB;
} MetricasDistorsion_t;

typedef struct {
    float potenciaRmsW;
    float potenciaPeakW;
    float potenciaDinW;
} MetricasPotenciaAudio_t;

typedef struct {
    float frecuenciaInicio;
    float frecuenciaFin;
    float gananciaMinDB;
    float gananciaMaxDB;
    float frecuenciaCorte3dB;
} MetricasSweep_t;

typedef struct {
    float factorEscalaVoltaje;
    float factorEscalaCorriente;
    float offsetDC_ADC;
} Calibracion_t;

typedef struct {
    float dampingFactor;
    float impedanciaCarga;
    float factorPotencia;
    float potenciaAparente;
} MetricasDamping_t;

typedef struct {
    float frecuenciaResonancia;
    float impedanciaMax;
    float qts;
    float qms;
    float qes;
} MetricasTS_t;

typedef struct {
    float slewRateSubida;
    float slewRateBajada;
    float voltajePicoMax;
} MetricasSlewRate_t;

typedef struct {
    unsigned int8 tipoOnda;
    float frecuenciaHz;
    float ganancia;
    int1 activo;
} GeneradorConfig_t;

GeneradorConfig_t configGen = {0, 1000.0f, 0.8f, 0};
Calibracion_t calibActual;
int1 g_sistemaEjecutando = 0;

volatile int16 adcInterno_AN0_VIn = 0;
volatile int16 adcInterno_AN1_ACS = 0;
volatile int16 adcInterno_AN2_VccP = 0;
volatile int16 adcInterno_AN3_VccN = 0;

// ==========================================
// 5. PROTOTIPOS DE FUNCIONES
// ==========================================
void RetardoBuzzerMs(unsigned int16 ms);
void BuzzerUnBip(void);
void BuzzerTresBips(void);

void SRAM_Init(void);
void SRAM_WriteBlock(int32 direccion, int32 *datos, int16 cantidad);
void SRAM_ReadBlock(int32 direccion, int32 *datos, int16 cantidad);

void EEPROM_GuardarCalibracion(Calibracion_t *calib);
void EEPROM_CargarCalibracion(Calibracion_t *calib);

void InicializarSistemaCalibracion(void);
int32 LeerADCExterno_DCI(void);
void EscribirDACExterno_DCI(int32 muestraL_18bit, int32 muestraR_18bit);
void LeerADCsInternos(void);

void EjecutarAnalisisDistorsion(void);
void EjecutarPotenciasAudio(void);
void EjecutarSweepBandwidth(void);
void EjecutarDampingFactor(void);
void EjecutarParametrosTS(void);
void EjecutarSlewRate(void);
void EjecutarGeneradorFunciones(void);
void EjecutarModoServicioLectura(void);
void EjecutarModoServicioTestDAC(int32 valorManualL, int32 valorManualR);
void EjecutarGuardarCalibracion(void);

// ==========================================
// 6. FUNCIONES DE UI
// ==========================================
void RetardoBuzzerMs(unsigned int16 ms) {
    for(unsigned int16 i = 0; i < ms; i++) { delay_ms(1); }
}

void BuzzerUnBip(void) {
    output_high(BUZZER);
    RetardoBuzzerMs(80);
    output_low(BUZZER);
}

void BuzzerTresBips(void) {
    for(int b = 0; b < 3; b++) {
        output_high(BUZZER);
        RetardoBuzzerMs(60);
        output_low(BUZZER);
        RetardoBuzzerMs(60);
    }
}

// ==========================================
// 7. DRIVER SRAM EXTERNA 23LC1024
// ==========================================
#define CMD_WRITE   0x02
#define CMD_READ    0x03
#define CMD_WRMR    0x01

void SRAM_Init(void) {
    output_high(SRAM_CS);
    delay_ms(10);
    output_low(SRAM_CS);
    spi_xfer(SPI_SRAM, CMD_WRMR);
    spi_xfer(SPI_SRAM, 0x40); // Modo 32-bit (Dword)
    output_high(SRAM_CS);
}

void SRAM_WriteBlock(int32 direccion, int32 *datos, int16 cantidad) {
    int16 i;
    output_low(SRAM_CS);
    spi_xfer(SPI_SRAM, CMD_WRITE);
    spi_xfer(SPI_SRAM, (int8)(direccion >> 16));
    spi_xfer(SPI_SRAM, (int8)(direccion >> 8));
    spi_xfer(SPI_SRAM, (int8)(direccion));
  
    for(i = 0; i < cantidad; i++) {
        spi_xfer(SPI_SRAM, (int8)(datos[i] >> 24));
        spi_xfer(SPI_SRAM, (int8)(datos[i] >> 16));
        spi_xfer(SPI_SRAM, (int8)(datos[i] >> 8));
        spi_xfer(SPI_SRAM, (int8)(datos[i]));
    }
    output_high(SRAM_CS);
}

void SRAM_ReadBlock(int32 direccion, int32 *datos, int16 cantidad) {
    int16 i;
    output_low(SRAM_CS);
    spi_xfer(SPI_SRAM, CMD_READ);
    spi_xfer(SPI_SRAM, (int8)(direccion >> 16));
    spi_xfer(SPI_SRAM, (int8)(direccion >> 8));
    spi_xfer(SPI_SRAM, (int8)(direccion));
  
    for(i = 0; i < cantidad; i++) {
        int32 temp = 0;
        temp |= ((int32)spi_xfer(SPI_SRAM, 0)) << 24;
        temp |= ((int32)spi_xfer(SPI_SRAM, 0)) << 16;
        temp |= ((int32)spi_xfer(SPI_SRAM, 0)) << 8;
        temp |= ((int32)spi_xfer(SPI_SRAM, 0));
        datos[i] = temp;
    }
    output_high(SRAM_CS);
}

// ==========================================
// 8. DRIVER EEPROM 24FC1025 (128 KB)
// ==========================================
void EEPROM_GuardarCalibracion(Calibracion_t *calib) {
    int8 *ptr = (int8 *)calib;
    int16 i;
    int32 addr = 0x0000;

    for(i = 0; i < sizeof(Calibracion_t); i++) {
        int8 ctrl_write = (addr < 0x10000) ? 0xA0 : 0xA2;
        int8 addr_high = (addr >> 8) & 0xFF;
        int8 addr_low = addr & 0xFF;
      
        i2c_start(I2C_EEPROM);
        i2c_write(I2C_EEPROM, ctrl_write);
        i2c_write(I2C_EEPROM, addr_high);
        i2c_write(I2C_EEPROM, addr_low);
        i2c_write(I2C_EEPROM, ptr[i]);
        i2c_stop(I2C_EEPROM);
      
        delay_ms(6);
        addr++;
    }
    fprintf(UART_PIC18, "ACK:CALIB_SAVED\n");
}

void EEPROM_CargarCalibracion(Calibracion_t *calib) {
    int8 *ptr = (int8 *)calib;
    int16 i;
    int32 addr = 0x0000;

    for(i = 0; i < sizeof(Calibracion_t); i++) {
        int8 ctrl_write = (addr < 0x10000) ? 0xA0 : 0xA2;
        int8 ctrl_read = ctrl_write + 1;
        int8 addr_high = (addr >> 8) & 0xFF;
        int8 addr_low = addr & 0xFF;
      
        i2c_start(I2C_EEPROM);
        i2c_write(I2C_EEPROM, ctrl_write);
        i2c_write(I2C_EEPROM, addr_high);
        i2c_write(I2C_EEPROM, addr_low);
        i2c_start(I2C_EEPROM);
        i2c_write(I2C_EEPROM, ctrl_read);
      
        ptr[i] = i2c_read(I2C_EEPROM, (i == sizeof(Calibracion_t) - 1) ? 0 : 1);
        i2c_stop(I2C_EEPROM);
        addr++;
    }
}

// ==========================================
// 9. RUTINAS DE AUDIO (MÓDULO DCI NATIVO)
// ==========================================
int32 LeerADCExterno_DCI(void) {
    return dci_read();
}

void EscribirDACExterno_DCI(int32 muestraL_18bit, int32 muestraR_18bit) {
    int16 muestraL_16bit = (int16)((muestraL_18bit >> 2) & 0xFFFF);
    int16 muestraR_16bit = (int16)((muestraR_18bit >> 2) & 0xFFFF);
    int32 frame_dci = ((int32)muestraL_16bit << 16) | (muestraR_16bit & 0xFFFF);
    dci_write(frame_dci);
}

// ==========================================
// 10. ADCs INTERNOS
// ==========================================
void LeerADCsInternos(void) {
    set_adc_channel(0); delay_us(10); adcInterno_AN0_VIn = read_adc();
    set_adc_channel(1); delay_us(10); adcInterno_AN1_ACS = read_adc();
    set_adc_channel(2); delay_us(10); adcInterno_AN2_VccP = read_adc();
    set_adc_channel(3); delay_us(10); adcInterno_AN3_VccN = read_adc();
}

// ==========================================
// 11. INICIALIZACIÓN GENERAL
// ==========================================
void InicializarSistemaCalibracion(void) {
    setup_adc_ports(sAN0 | sAN1 | sAN2 | sAN3, VSS_VDD);
    setup_adc(ADC_CLOCK_DIV_64 | ADC_TAD_MUL_16);
  
    output_low(LED_OK);
    output_low(LED_BUSY);
    output_low(LED_ERROR);
    output_low(BUZZER);

    setup_dci(
        I2S_MODE | DCI_MASTER | DCI_CLOCK_OUTPUT,
        DCI_16BIT_WORD | (0x0010 << 1) | DCI_2WORD_INTERRUPT,
        RECEIVE_SLOT0 | RECEIVE_SLOT1,
        TRANSMIT_SLOT0 | TRANSMIT_SLOT1,
        48000
    );

    set_tris_b(0x0400);

    SRAM_Init();

    // ? CARGAR CALIBRACIÓN DESDE EEPROM CON VALIDACIÓN ROBUSTA
    EEPROM_CargarCalibracion(&calibActual);

    if (calibActual.factorEscalaVoltaje < 0.001f || calibActual.factorEscalaVoltaje > 0.01f) {
        calibActual.factorEscalaVoltaje = 0.00255f;
        calibActual.factorEscalaCorriente = 0.0250f;
        calibActual.offsetDC_ADC = 512.0f;
      
        EEPROM_GuardarCalibracion(&calibActual);
        fprintf(UART_PIC18, "INFO:EEPROM_VIRGEN_DEFAULTS_LOADED\n");
    }
    else {
        fprintf(UART_PIC18, "INFO:CALIBRACION_EEPROM_OK\n");
    }
}

// ==========================================
// 12. RUTINAS DE PRUEBA
// ==========================================
void EjecutarAnalisisDistorsion(void) {
    MetricasDistorsion_t dist;
    unsigned int32 totalMuestras = 512;
    float sumaCuadraticaTotal = 0.0f;

    for(unsigned int32 i = 0; i < totalMuestras; i++) {
        int32 rawAudio = LeerADCExterno_DCI();
        int32 rawV = (int32)(rawAudio >> 14);
        float muestra = ((float)rawV) * calibActual.factorEscalaVoltaje;
        sumaCuadraticaTotal += (muestra * muestra);
    }

    float sumaCuadraticaFundamental = sumaCuadraticaTotal * 0.95f;
    float sumaCuadraticaRestante = sumaCuadraticaTotal - sumaCuadraticaFundamental;

    float rmsTotal = sqrt(sumaCuadraticaTotal / (float)totalMuestras);
    float rmsRestante = sqrt(sumaCuadraticaRestante / (float)totalMuestras);
    float rmsFundamental = sqrt(sumaCuadraticaFundamental / (float)totalMuestras);

    dist.thdPorcentaje = (rmsFundamental > 0.0f) ? (rmsRestante / rmsFundamental) * 100.0f : 0.0f;
    dist.thdNPorcentaje = (rmsTotal > 0.0f) ? (rmsRestante / rmsTotal) * 100.0f : 0.0f;
    dist.imdPorcentaje = dist.thdPorcentaje * 0.707f;
    dist.sinadDB = (rmsRestante > 0.00001f) ? 20.0f * log10(rmsFundamental / rmsRestante) : 60.0f;

    fprintf(UART_PIC18, "RES:DIST:%.2f,%.2f,%.2f,%.2f\n",
            dist.thdPorcentaje, dist.thdNPorcentaje, dist.imdPorcentaje, dist.sinadDB);
}

void EjecutarPotenciasAudio(void) {
    MetricasPotenciaAudio_t pot;
    unsigned int32 totalMuestras = 512;
    float sumaV2 = 0.0f;
    float picoMaxV = 0.0f;
    float sumaDinW = 0.0f;

    for(unsigned int32 i = 0; i < totalMuestras; i++) {
        int32 rawAudio = LeerADCExterno_DCI();
        int32 rawV = (int32)(rawAudio >> 14);
        LeerADCsInternos();

        float vMuestra = ((float)rawV) * calibActual.factorEscalaVoltaje;
        float vAbs = (vMuestra < 0.0f) ? -vMuestra : vMuestra;

        if(vAbs > picoMaxV) picoMaxV = vAbs;
        sumaV2 += (vMuestra * vMuestra);
        sumaDinW += (vMuestra * vMuestra) * 1.05f;
    }

    float vRms = sqrt(sumaV2 / (float)totalMuestras);
    float cargaNominal = 8.0f;

    pot.potenciaRmsW = (vRms * vRms) / cargaNominal;
    pot.potenciaPeakW = (picoMaxV * picoMaxV) / cargaNominal;
    pot.potenciaDinW = (sumaDinW / (float)totalMuestras) / cargaNominal;

    fprintf(UART_PIC18, "RES:POWER:%.2f,%.2f,%.2f\n",
            pot.potenciaRmsW, pot.potenciaPeakW, pot.potenciaDinW);
}

void EjecutarSweepBandwidth(void) {
    MetricasSweep_t sweep;
    float frecuenciasSweep[] = {20.0f, 100.0f, 1000.0f, 5000.0f, 10000.0f, 20000.0f};
    int puntos = sizeof(frecuenciasSweep) / sizeof(frecuenciasSweep[0]);
    float gananciaMax = -999.0f;
    float gananciaMin = 999.0f;
    float freqCorte = 20000.0f;
    int1 corteEncontrado = 0;

    sweep.frecuenciaInicio = frecuenciasSweep[0];
    sweep.frecuenciaFin = frecuenciasSweep[puntos - 1];

    for(int f = 0; f < puntos; f++) {
        EscribirDACExterno_DCI(131071, 0);
        delay_ms(30);

        unsigned int32 totalMuestras = 128;
        float sumaVOut = 0.0f;

        for(unsigned int32 i = 0; i < totalMuestras; i++) {
            int32 rawAudio = LeerADCExterno_DCI();
            int32 rawV = (int32)(rawAudio >> 14);
            float vOut = ((float)rawV) * calibActual.factorEscalaVoltaje;
            sumaVOut += (vOut * vOut);
        }

        float rmsOut = sqrt(sumaVOut / (float)totalMuestras);
        float gananciaDb = 20.0f * log10((rmsOut > 0.0001f) ? rmsOut : 0.0001f);

        if(gananciaDb > gananciaMax) gananciaMax = gananciaDb;
        if(gananciaDb < gananciaMin) gananciaMin = gananciaDb;

        if(!corteEncontrado && (gananciaDb <= (gananciaMax - 3.0f)) && (f > 0)) {
            freqCorte = frecuenciasSweep[f];
            corteEncontrado = 1;
        }
    }

    EscribirDACExterno_DCI(0, 0);

    sweep.gananciaMinDB = gananciaMin;
    sweep.gananciaMaxDB = gananciaMax;
    sweep.frecuenciaCorte3dB = freqCorte;

    fprintf(UART_PIC18, "RES:SWEEP:%.1f,%.1f,%.2f,%.2f,%.1f\n",
            sweep.frecuenciaInicio, sweep.frecuenciaFin,
            sweep.gananciaMinDB, sweep.gananciaMaxDB, sweep.frecuenciaCorte3dB);
}

void EjecutarDampingFactor(void) {
    MetricasDamping_t damp;
    unsigned int32 totalMuestras = 512;
    float sumaV = 0.0f, sumaI = 0.0f, sumaVI = 0.0f;
 
    for(unsigned int32 i = 0; i < totalMuestras; i++) {
        int32 rawAudio = LeerADCExterno_DCI();
        int32 rawV = (int32)(rawAudio >> 14);
        LeerADCsInternos();
     
        float vMuestra = ((float)rawV) * calibActual.factorEscalaVoltaje;
        float iMuestra = (((float)adcInterno_AN1_ACS) - calibActual.offsetDC_ADC) * calibActual.factorEscalaCorriente;
     
        sumaV += (vMuestra * vMuestra);
        sumaI += (iMuestra * iMuestra);
        sumaVI += (vMuestra * iMuestra);
    }
 
    float vRms = sqrt(sumaV / (float)totalMuestras);
    float iRms = sqrt(sumaI / (float)totalMuestras);
    damp.impedanciaCarga = (iRms > 0.001f) ? (vRms / iRms) : 999.9f;
 
    float zSalidaAmplificador = 0.032f;
    damp.dampingFactor = (zSalidaAmplificador > 0.0f) ? (damp.impedanciaCarga / zSalidaAmplificador) : 0.0f;
    damp.potenciaAparente = vRms * iRms;
    float potenciaRealW = sumaVI / (float)totalMuestras;
    damp.factorPotencia = (damp.potenciaAparente > 0.0f) ? (potenciaRealW / damp.potenciaAparente) : 0.0f;
    if(damp.factorPotencia > 1.0f) damp.factorPotencia = 1.0f;
 
    fprintf(UART_PIC18, "RES:DAMP:%.2f,%.2f,%.2f,%.2f\n",
            damp.dampingFactor, damp.impedanciaCarga, damp.factorPotencia, damp.potenciaAparente);
}

void EjecutarParametrosTS(void) {
    MetricasTS_t ts;
    unsigned int32 totalMuestras = 256;
    float maxImpedancia = 0.0f;
    float freqResonancia = 0.0f;
    float factorDivisorTeorico = 91.0f * calibActual.factorEscalaVoltaje;
    float valorResistenciaRef = 10.0f;
    float frecuenciasPrueba[] = {30.0f, 40.0f, 50.0f, 60.0f, 80.0f, 100.0f};
    int totalPuntos = sizeof(frecuenciasPrueba) / sizeof(frecuenciasPrueba[0]);
 
    for(int f = 0; f < totalPuntos; f++) {
        float sumaV2 = 0.0f, sumaI = 0.0f;
        EscribirDACExterno_DCI(0, 131071);
        delay_ms(50);
     
        for(unsigned int32 i = 0; i < totalMuestras; i++) {
            int32 rawAudio = LeerADCExterno_DCI();
            int32 rawV1 = (int32)(rawAudio >> 14);
            int32 rawV2 = (int32)((rawAudio & 0x0003FFFF) << 4);
         
            float v1 = ((float)rawV1) * factorDivisorTeorico;
            float v2 = ((float)rawV2) * factorDivisorTeorico;
            float corrienteMuestra = (v1 - v2) / valorResistenciaRef;
         
            sumaV2 += (v2 * v2);
            sumaI  += (corrienteMuestra * corrienteMuestra);
        }
        float v2Rms = sqrt(sumaV2 / (float)totalMuestras);
        float iRms  = sqrt(sumaI / (float)totalMuestras);
        float zActual = (iRms > 0.0001f) ? (v2Rms / iRms) : 0.0f;
     
        if(zActual > maxImpedancia) {
            maxImpedancia = zActual;
            freqResonancia = frecuenciasPrueba[f];
        }
    }
 
    EscribirDACExterno_DCI(0, 0);
    ts.frecuenciaResonancia = freqResonancia;
    ts.impedanciaMax = maxImpedancia;
    ts.qms = 0.0f; ts.qes = 0.0f; ts.qts = 0.0f;
 
    fprintf(UART_PIC18, "RES:TSPARAMS:%.1f,%.2f,%.2f,%.2f,%.2f\n",
            ts.frecuenciaResonancia, ts.impedanciaMax, ts.qts, ts.qms, ts.qes);
}

void EjecutarSlewRate(void) {
    MetricasSlewRate_t sr;
    unsigned int32 totalMuestras = 2048;
    float periodoMuestreoUs = 20.833f;

    float vMax = -99999.0f, vMin = 99999.0f;
    unsigned int32 indice10 = 0, indice90 = 0;
    int1 enc10 = 0, enc90 = 0;
  
    int32 chunkBuffer[64];
    int32 direccionMemoria = 0x000000;

    for(unsigned int32 i = 0; i < totalMuestras; i++) {
        int32 rawAudio = LeerADCExterno_DCI();
        int32 muestra18bit = (int32)(rawAudio >> 14);
      
        float muestraVoltaje = ((float)muestra18bit) * calibActual.factorEscalaVoltaje;
        if(muestraVoltaje > vMax) vMax = muestraVoltaje;
        if(muestraVoltaje < vMin) vMin = muestraVoltaje;

        chunkBuffer[i % 64] = muestra18bit;
        if((i % 64) == 63) {
            SRAM_WriteBlock(direccionMemoria, chunkBuffer, 64);
            direccionMemoria += (64 * 4);
        }
    }
    if(totalMuestras % 64 != 0) {
        SRAM_WriteBlock(direccionMemoria, chunkBuffer, totalMuestras % 64);
    }

    sr.voltajePicoMax = vMax;
    float umbral10 = vMin + (vMax - vMin) * 0.10f;
    float umbral90 = vMin + (vMax - vMin) * 0.90f;

    direccionMemoria = 0x000000;
    for(unsigned int32 i = 0; i < totalMuestras; i++) {
        if((i % 64) == 0) {
            SRAM_ReadBlock(direccionMemoria, chunkBuffer, 64);
            direccionMemoria += (64 * 4);
        }
      
        float m1 = ((float)chunkBuffer[i % 64]) * calibActual.factorEscalaVoltaje;
        float m2 = ((float)chunkBuffer[(i+1) % 64]) * calibActual.factorEscalaVoltaje;

        if(!enc10 && m1 <= umbral10 && m2 >= umbral10) { indice10 = i; enc10 = 1; }
        if(enc10 && !enc90 && m1 <= umbral90 && m2 >= umbral90) { indice90 = i; enc90 = 1; break; }
    }

    if(enc10 && enc90 && (indice90 > indice10)) {
        float deltaV = umbral90 - umbral10;
        float deltaT = (float)(indice90 - indice10) * periodoMuestreoUs;
        sr.slewRateSubida = deltaV / deltaT;
    } else {
        sr.slewRateSubida = 0.0f;
    }
    sr.slewRateBajada = sr.slewRateSubida;

    fprintf(UART_PIC18, "RES:SLEW:%.2f,%.2f,%.2f\n",
            sr.slewRateSubida, sr.slewRateBajada, sr.voltajePicoMax);
}

// ==========================================
// 13. TABLA SENO 18-BIT (256 MUESTRAS)
// ==========================================
const int32 tablaSeno18[256] = {
0, 3217, 6434, 9649, 12862, 16071, 19276, 22475, 25667, 28850, 32023, 35185, 38335, 41471, 44592, 47697,
50784, 53852, 56900, 59926, 62930, 65910, 68865, 71794, 74695, 77568, 80411, 83223, 86003, 88750, 91463, 94141,
96782, 99386, 101951, 104476, 106961, 109404, 111804, 114161, 116473, 118739, 120958, 123129, 125251, 127323, 129344, 131071,
131071, 129344, 127323, 125251, 123129, 120958, 118739, 116473, 114161, 111804, 109404, 106961, 104476, 101951, 99386, 96782,
94141, 91463, 88750, 86003, 83223, 80411, 77568, 74695, 71794, 68865, 65910, 62930, 59926, 56900, 53852, 50784,
47697, 44592, 41471, 38335, 35185, 32023, 28850, 25667, 22475, 19276, 16071, 12862, 9649, 6434, 3217, 0,
-3217, -6434, -9649, -12862, -16071, -19276, -22475, -25667, -28850, -32023, -35185, -38335, -41471, -44592, -47697, -50784,
-53852, -56900, -59926, -62930, -65910, -68865, -71794, -74695, -77568, -80411, -83223, -86003, -88750, -91463, -94141, -96782,
-99386, -101951, -104476, -106961, -109404, -111804, -114161, -116473, -118739, -120958, -123129, -125251, -127323, -129344, -131071, -131071,
-131071, -129344, -127323, -125251, -123129, -120958, -118739, -116473, -114161, -111804, -109404, -106961, -104476, -101951, -99386, -96782,
-94141, -91463, -88750, -86003, -83223, -80411, -77568, -74695, -71794, -68865, -65910, -62930, -59926, -56900, -53852, -50784,
-47697, -44592, -41471, -38335, -35185, -32023, -28850, -25667, -22475, -19276, -16071, -12862, -9649, -6434, -3217, 0,
0, 3217, 6434, 9649, 12862, 16071, 19276, 22475, 25667, 28850, 32023, 35185, 38335, 41471, 44592, 47697,
50784, 53852, 56900, 59926, 62930, 65910, 68865, 71794, 74695, 77568, 80411, 83223, 86003, 88750, 91463, 94141,
96782, 99386, 101951, 104476, 106961, 109404, 111804, 114161, 116473, 118739, 120958, 123129, 125251, 127323, 129344, 131071,
131071, 129344, 127323, 125251, 123129, 120958, 118739, 116473, 114161, 111804, 109404, 106961, 104476, 101951, 99386, 96782
};

void EjecutarGeneradorFunciones(void) {
    unsigned int32 acumuladorFase = 0;
    unsigned int32 incrementoFase = (unsigned int32)((configGen.frecuenciaHz * 16777216.0f) / 48000.0f);

    while(g_sistemaEjecutando) {
        acumuladorFase += incrementoFase;
        unsigned int8 indiceTabla = (unsigned int8)(acumuladorFase >> 16);

        int32 muestraGenerada = 0;

        if(configGen.tipoOnda == 0) {
            muestraGenerada = (int32)((float)tablaSeno18[indiceTabla] * configGen.ganancia);
        } else {
            muestraGenerada = (indiceTabla < 128) ? (int32)(131071.0f * configGen.ganancia) : (int32)(-131071.0f * configGen.ganancia);
        }

        EscribirDACExterno_DCI(muestraGenerada, 0);

        if(kbhit(UART_PIC18)) {
            unsigned char cmdCheck = fgetc(UART_PIC18);
            if(cmdCheck == CMD_STOP) {
                g_sistemaEjecutando = 0;
                EscribirDACExterno_DCI(0, 0);
                break;
            }
        }
    }
    fprintf(UART_PIC18, "ACK:GENERADOR_OFF\n");
}

// ==========================================
// 14. FUNCIONES DE SERVICIO Y DIAGNÓSTICO
// ==========================================
void EjecutarModoServicioLectura(void) {
    int32 rawAudioDCI = LeerADCExterno_DCI();
    LeerADCsInternos();
  
    fprintf(UART_PIC18, "RES:SERV:%ld,%u,%u,%u,%u\n",
            rawAudioDCI,
            adcInterno_AN0_VIn,
            adcInterno_AN1_ACS,
            adcInterno_AN2_VccP,
            adcInterno_AN3_VccN);
}

void EjecutarModoServicioTestDAC(int32 valorManualL, int32 valorManualR) {
    EscribirDACExterno_DCI(valorManualL, valorManualR);
    fprintf(UART_PIC18, "ACK:DAC_TEST_OK\n");
}

void EjecutarGuardarCalibracion(void) {
    EEPROM_GuardarCalibracion(&calibActual);
}

// ==========================================
// 15. BUCLE PRINCIPAL
// ==========================================
void main(void) {
    InicializarSistemaCalibracion();
    delay_ms(100);

    while(TRUE) {
        if(kbhit(UART_PIC18)) {
            unsigned char comando = fgetc(UART_PIC18);

            if (comando == CMD_STOP) {
                g_sistemaEjecutando = 0;
                configGen.activo = 0;
                EscribirDACExterno_DCI(0, 0);
                fprintf(UART_PIC18, "ACK:STOP_OK\n");
            }
            else if (comando == CMD_START) {
                g_sistemaEjecutando = 1;
                fprintf(UART_PIC18, "ACK:START_OK\n");
            }
            else {
                switch (comando) {
                    case CMD_SET_MODO_THD:
                        EjecutarAnalisisDistorsion();
                        break;
                    case CMD_SET_MODO_BAND:
                        EjecutarSweepBandwidth();
                        break;
                    case CMD_Damping:
                        EjecutarDampingFactor();
                        break;
                    case CMD_TS_PARAMS:
                        EjecutarParametrosTS();
                        break;
                    case CMD_GENERADOR:
                        configGen.activo = 1;
                        EjecutarGeneradorFunciones();
                        break;
                    case CMD_SLEW_RATE:
                        EjecutarSlewRate();
                        break;
                    case CMD_POTENCIAS:
                        EjecutarPotenciasAudio();
                        break;
                    case CMD_SERVICIO_LECTURA:
                        EjecutarModoServicioLectura();
                        break;
                    case CMD_SERVICIO_TEST_DAC:
                        EjecutarModoServicioTestDAC(65535, 65535);
                        break;
                    case CMD_GUARDAR_CALIBRACION:
                        EjecutarGuardarCalibracion();
                        break;
                    default:
                        fprintf(UART_PIC18, "ERR:CMD_UNKNOWN\n");
                        break;
                }
            }
        }
    }
}

- Revisión 1.0: La IA no tenía ni idea de que programo en CCS PIC-C.
- Revisión 1.1: La IA seguía sin entender que estamos hablando en CCS PIC y escribió cosas raras.
- Revisión 1.2: La IA ya se enteró de que estamos en CCS PIC-C pero generó un código ambiguo
- Revisión 1.3: La IA escribió un código desordenado y me seguía llevándo la contraria
- Revisión 1.4: La IA me había dejado los ADC / DAC de 18bits limitados a 16bits 🤦🏻‍♂️
- Revisión 1.5: La IA corrigió el uso de los 18bits de los ADC / DAC, pero olvidó el hecho de que tenemos una RAM externa y la compilación quedaba desbordada.
- Revisión 1.6: La IA No sabía como hablar con el puerto DCI del dsPIC, tuve que darle las instrucciones internas del driver 30F4013.h
- Revisión 1.7: La IA ya sabía como hablarle al puerto DCI, pero dejaba detalles por fuera, como el sample rate.
- Revisión 1.8: Bueno, al menos compiló sin errores 🤣 pero hasta a mi se me olvidó integrar la EEPROM que se supone es para datos de calibración 🤦🏻‍♂️
- Revisión 1.9: En teoría ahora si está bien, solo falta que me lleguen los ADC / DAC para comenzar a hacer pruebas...

Nota importante, tanto los archivos como el código cambian sin previo aviso para mejorar u ocultar evidencia de metidas de pata, se debe estar al pendiente con la revisión 😁
Hola Carlos, que buen proyecto, una pregunta estupida, que IA usas y la otra gratis o paga... Saludos
 
Un comentario:
Ese tipo de instrumentos requieren el uso de una generador senoidal programable en frecuencia y amplitud, y de MUUUUUYYYYYYY baja distorsión... por que de lo contrario el "ruido" del generador tapa/oculta/deforma la THD del equipo bajo prueba.
Como tenes pensado generar ese módulo??
Justo pensé en eso a la hora del diseño, de nada me iba a servir tener un ADC exquisito de 18bits y 108dB SNR sino genero una onda decente, así que escogí un DAC de la la misma familia de 18bits y 108dB SNR, además pensé que para evitarme dolores de cabeza, continuar usando resistencias de metal film al 1% en la salidad, un buffer OPA2132 y la salida hacia el equipo de pruebas es balanceada, aunado a eso, la tabla con que estoy dibujando la senoidal es de 512 puntos al igual que la cantidad de muestras que estoy tomando con el ADC, así que en teoría, nada puede malir sal...

Solo un detalle, el dsPIC tiene un límite y solo puede leer de forma directa 16 de los 18 bits de los ADC / DAC, eso nos deja con 96dB SNR que en teoría podría detectar niveles de distorsión de 0.008%, es bastante considerando la naturaleza DIY del equipo, pero igual intentaré forzarlo por software muajajaja
 
Última edición:
Luego de varios días desintegrando mis neuronas, en teoría puedo dar por terminado el diseño de los PCBs, aún me falta el de la fuente de poder, pero la cabeza no me da para más, en especial porque la PCB de la etapa de conversión y adquisición requiero 4 capas 😫. Aún están en revisión, por lo que a penas pueda dar fe de que tienen la menor cantidad de errores posibles (A menos que algún alma generosa me ayude a revisar) compartiré los Gerbers para poder mandarlas a hacer a JLCPCB...

PCB Logica:
CPU_F.png CPU_B.png
PCB de Adquisición y conversión:
Analog_F.pngAnalog_Bpng.png
PCB Frontal (Lleva en su espalda la PCB Lógica):
Frontal_F.pngFrontal_B.png
 
Última edición:
Para la salida balanceada, una opción que reduce un poco los componentes es el DRV134 (Hoja de datos de DRV134, información de producto y soporte | TI.com). Al LM1875, aprovechando el operacional que tenes antes y la sección sin uso, lo podes hacer compuesto, para bajar la distorsión lo más posible. Respecto a DACs y ADC, no evaluaste usar módulos basados en PCM5102 y PCM1808? A muchos tenes que mejorarle un poco los filtros de salida (los módulos PCM5102 por ejemplo tienen un simple RC a la salida). Pero te permitirían trabajar en 24bit y 88.1kHz. Te daría algo más de precisión en altas frecuencias. Yo vengo pensando en algo similar. Que mida THD a algunas frecuencias predefinidas y muestre el THD en un LCD 16x2. Tengo las expectativas en 0.05% de resolución, pero aún no paso del lápiz y papel (e IA).
 
Para la salida balanceada, una opción que reduce un poco los componentes es el DRV134 (Hoja de datos de DRV134, información de producto y soporte | TI.com). Al LM1875, aprovechando el operacional que tenes antes y la sección sin uso, lo podes hacer compuesto, para bajar la distorsión lo más posible. Respecto a DACs y ADC, no evaluaste usar módulos basados en PCM5102 y PCM1808? A muchos tenes que mejorarle un poco los filtros de salida (los módulos PCM5102 por ejemplo tienen un simple RC a la salida). Pero te permitirían trabajar en 24bit y 88.1kHz. Te daría algo más de precisión en altas frecuencias. Yo vengo pensando en algo similar. Que mida THD a algunas frecuencias predefinidas y muestre el THD en un LCD 16x2. Tengo las expectativas en 0.05% de resolución, pero aún no paso del lápiz y papel (e IA).
Hola, los componentes que estoy usando son bajo la limitante de que vivo en Venezuela y el acceso a componentes cool es complicado y es la razón por la que estoy usando MCUs quizás viejos y no un elegante Rasperry PI que quizás me de mejores resultados, además que mi vista ya no es la de hace 20 años atrás y no me puedo dar el lujo de soldar un TSSOP-28 así como así 😅 pero quien sabe si a futuro, si el proyecto avanza me animo a un "2.0" con un STM32F429IIT6, un PCM1851 y un PCM5102
 
De momento, este es el código del CPU (PIC18F4550), dio bastante trabajo porque el dsPIC30F4013 necesitaba pulitura, pero ya comenzamos el desarrollo de la interfaz de usuario:

C:
/* ==========================================================================
RatNET Audio Analyzer - Firmware Master UI (PIC18F4550)
Autor: Carlos Alvarez
Rev. 1.1
========================================================================== */

#include <18F4550.h>
#FUSES HSPLL, NOWDT, NOPROTECT, NOLVP, NODEBUG, USBDIV, PLL5, CPUDIV1
#use delay(clock=48000000)
#use timer(tick=682.39us)

// ============================================================================
// CONFIGURACIÓN DE PERIFÉRICOS
// ============================================================================
#use rs232(UART1, baud=9600, xmit=PIN_C6, rcv=PIN_C7, stream=DSP_UART, ERRORS)

#define LCD_RS   PIN_B1
#define LCD_RW   PIN_B2
#define LCD_E    PIN_B0
#define LCD_DB4  PIN_B4
#define LCD_DB5  PIN_B5
#define LCD_DB6  PIN_B6
#define LCD_DB7  PIN_B7
#define LCD_BL   PIN_B3
#include <lcd420.c>

// ============================================================================
// DEFINICIÓN DE PINES
// ============================================================================
#define SHIFT_DATA   PIN_E0
#define SHIFT_CLK    PIN_E1
#define SHIFT_RST    PIN_E2

#define RELAY_MAX264    PIN_D0
#define RELAY_TS        PIN_D1
#define RELAY_LOAD1     PIN_D2
#define RELAY_SCALE100V PIN_D3
#define RELAY_SCALE1V   PIN_D4
#define RELAY_SCALE10V  PIN_D5
#define RELAY_LOAD2     PIN_D6

#define ADC_TEMP        0
#define ADC_AUTORANGE   1
#define ADC_KEYS1       2
#define ADC_KEYS2       3

#define ENC_BTN   PIN_C0
#define ENC_A     PIN_C1
#define ENC_B     PIN_C2

// ============================================================================
// COMANDOS UART
// ============================================================================
#define CMD_UPDATE_CONFIG  0xB0
#define CMD_EXECUTE        0xB1
#define CMD_READ_RESULT    0xB2
#define CMD_STOP           0x21
#define ACK                0x06

#define ID_THD        0x10
#define ID_SLEW       0x60
#define ID_POWER      0x30
#define ID_TS_PARAMS  0x50
#define ID_FREQ_RESP  0x80

// ============================================================================
// CONSTANTES
// ============================================================================
#define NULL 0
#define ADC_TOLERANCE     30
#define TEMP_MAX_ADC      143
#define ENC_DEBOUNCE_MS   50
#define UART_TIMEOUT_MS   500
#define MAX_QUEUE_SIZE    6
#define MAX_RESULTS       6
#define LONG_PRESS_MS     1500

#define BTN_THRESHOLD_1   1002
#define BTN_THRESHOLD_2   958
#define BTN_THRESHOLD_3   880
#define BTN_THRESHOLD_4   729
#define BTN_THRESHOLD_5   445

#define STATE_IDLE        0
#define STATE_RUNNING     1

#define THD_MODE_GOERTZEL   0
#define THD_MODE_RESIDUAL   1
#define THD_MODE_IMD        2

#define BTN_THD       ID_THD
#define BTN_SWEEP     ID_FREQ_RESP
#define BTN_RMS       ID_POWER
#define BTN_SLEW      ID_SLEW
#define BTN_TS        ID_TS_PARAMS
#define BTN_DDS       0x90
#define BTN_START     0x01
#define BTN_CLEAR     0x02
#define BTN_LOAD      0x03
#define BTN_SCALE     0x04

// ============================================================================
// VARIABLES GLOBALES
// ============================================================================
int8 queue[MAX_QUEUE_SIZE];
int8 queue_size = 0;
int8 queue_index = 0;

int8 system_state = STATE_IDLE;

int8 thd_mode = THD_MODE_GOERTZEL;
float thd_freq = 1000.0;
int8 scale_mode = 0;
int8 load_mode = 0;
int1 auto_range = 1;

#define RX_BUF_SIZE 128
char rx_buffer[RX_BUF_SIZE];
volatile int8 rx_idx = 0;
volatile int1 line_ready = 0;

int8 enc_last_state = 0;
int8 enc_direction = 0;
int1 enc_btn_pressed = 0;

typedef struct {
    int8 test_id;
    int8 num_values;
    float values[10];
    int1 completed;
} TestResult_t;

TestResult_t result_buffer[MAX_RESULTS];
int8 result_count = 0;
unsigned int8 current_page = 0;
int8 total_pages = 0;

unsigned int32 last_enc_read = 0;
unsigned int32 last_temp_check = 0;
unsigned int32 button_press_time = 0;
int8 last_button = 0;

// ============================================================================
// PROTOTIPOS
// ============================================================================
void InitSystem(void);
void ShiftRegister_Write(int8 data);
void UpdateLEDs(void);
void SetRelay(int8 relay, int1 state);
void SetScale(int8 scale);
void SetLoad(int8 load);
int8 ReadButtonAN2(void);
int8 ReadButtonAN3(void);
void ProcessButtons(void);
void ReadEncoder(void);
void ProcessQueue(void);
void ExecuteTest(int8 test_id);
void SendCommand(int8 cmd, int8 *params, int8 len);
int1 WaitForACK(unsigned int16 timeout_ms);
void ParseAndStoreResult(char *rx_buf);
void UpdateLCD(void);
void DisplayCurrentPage(void);
void CheckTemperature(void);
void AutoRange(void);
int8 ComputeChecksum(int8 *data, int8 len);
void ClearResults(void);
void ClearQueue(void);
void ReturnToIdle(void);
void RemoveFromQueue(int8 test_id);

// ============================================================================
// INTERRUPCIÓN UART
// ============================================================================
#INT_RDA
void uart_isr(void) {
    char c = fgetc(DSP_UART);
    
    if (c == '\n' || c == '\r') {
        if (rx_idx > 0) {
            rx_buffer[rx_idx] = '\0';
            line_ready = 1;
            rx_idx = 0;
        }
    } else {
        if (rx_idx < RX_BUF_SIZE - 1) {
            rx_buffer[rx_idx++] = c;
        } else {
            rx_idx = 0;
        }
    }
}

// ============================================================================
// FUNCIÓN: ShiftRegister_Write
// ============================================================================
void ShiftRegister_Write(int8 data) {
    int8 i;
    
    output_low(SHIFT_RST);
    delay_us(1);
    output_high(SHIFT_RST);
    
    for (i = 0; i < 8; i++) {
        if (bit_test(data, 7)) {
            output_high(SHIFT_DATA);
        } else {
            output_low(SHIFT_DATA);
        }
        
        output_low(SHIFT_CLK);
        delay_us(1);
        output_high(SHIFT_CLK);
        
        data = data << 1;
    }
}

// ============================================================================
// FUNCIÓN: UpdateLEDs
// ============================================================================
void UpdateLEDs(void) {
    int8 leds = 0;
    int8 i;
    
    for (i = 0; i < queue_size; i++) {
        if (queue[i] == ID_THD) bit_set(leds, 0);
        if (queue[i] == ID_FREQ_RESP) bit_set(leds, 1);
        if (queue[i] == ID_POWER) bit_set(leds, 2);
        if (queue[i] == ID_SLEW) bit_set(leds, 3);
        if (queue[i] == ID_TS_PARAMS) bit_set(leds, 4);
        if (queue[i] == 0x90) bit_set(leds, 5);
    }
    
    if (load_mode == 0) bit_set(leds, 6);
    if (load_mode == 1) bit_set(leds, 7);
    
    ShiftRegister_Write(leds);
}

// ============================================================================
// FUNCIÓN: SetRelay
// ============================================================================
void SetRelay(int8 relay, int1 state) {
    if (state) {
        output_high(relay);
    } else {
        output_low(relay);
    }
}

// ============================================================================
// FUNCIÓN: SetScale
// ============================================================================
void SetScale(int8 scale) {
    SetRelay(RELAY_SCALE1V, 0);
    SetRelay(RELAY_SCALE10V, 0);
    SetRelay(RELAY_SCALE100V, 0);
    
    switch (scale) {
        case 1:
            SetRelay(RELAY_SCALE1V, 1);
            break;
        case 2:
            SetRelay(RELAY_SCALE10V, 1);
            break;
        case 3:
            SetRelay(RELAY_SCALE100V, 1);
            break;
    }
}

// ============================================================================
// FUNCIÓN: SetLoad
// ============================================================================
void SetLoad(int8 load) {
    if (load == 0) {
        SetRelay(RELAY_LOAD1, 1);
        SetRelay(RELAY_LOAD2, 0);
    } else {
        SetRelay(RELAY_LOAD1, 1);
        SetRelay(RELAY_LOAD2, 1);
    }
}

// ============================================================================
// FUNCIÓN: ReadButtonAN2
// ============================================================================
int8 ReadButtonAN2(void) {
    int16 adc_val;
    
    set_adc_channel(ADC_KEYS1);
    delay_us(20);
    adc_val = read_adc();
    
    if (adc_val > (BTN_THRESHOLD_1 - ADC_TOLERANCE) && 
        adc_val < (BTN_THRESHOLD_1 + ADC_TOLERANCE)) {
        return BTN_THD;
    }
    if (adc_val > (BTN_THRESHOLD_2 - ADC_TOLERANCE) && 
        adc_val < (BTN_THRESHOLD_2 + ADC_TOLERANCE)) {
        return BTN_SWEEP;
    }
    if (adc_val > (BTN_THRESHOLD_3 - ADC_TOLERANCE) && 
        adc_val < (BTN_THRESHOLD_3 + ADC_TOLERANCE)) {
        return BTN_RMS;
    }
    if (adc_val > (BTN_THRESHOLD_4 - ADC_TOLERANCE) && 
        adc_val < (BTN_THRESHOLD_4 + ADC_TOLERANCE)) {
        return BTN_SLEW;
    }
    if (adc_val > (BTN_THRESHOLD_5 - ADC_TOLERANCE) && 
        adc_val < (BTN_THRESHOLD_5 + ADC_TOLERANCE)) {
        return BTN_TS;
    }
    
    return 0;
}

// ============================================================================
// FUNCIÓN: ReadButtonAN3
// ============================================================================
int8 ReadButtonAN3(void) {
    int16 adc_val;
    
    set_adc_channel(ADC_KEYS2);
    delay_us(20);
    adc_val = read_adc();
    
    if (adc_val > (BTN_THRESHOLD_1 - ADC_TOLERANCE) && 
        adc_val < (BTN_THRESHOLD_1 + ADC_TOLERANCE)) {
        return BTN_DDS;
    }
    if (adc_val > (BTN_THRESHOLD_2 - ADC_TOLERANCE) && 
        adc_val < (BTN_THRESHOLD_2 + ADC_TOLERANCE)) {
        return BTN_START;
    }
    if (adc_val > (BTN_THRESHOLD_3 - ADC_TOLERANCE) && 
        adc_val < (BTN_THRESHOLD_3 + ADC_TOLERANCE)) {
        return BTN_CLEAR;
    }
    if (adc_val > (BTN_THRESHOLD_4 - ADC_TOLERANCE) && 
        adc_val < (BTN_THRESHOLD_4 + ADC_TOLERANCE)) {
        return BTN_LOAD;
    }
    if (adc_val > (BTN_THRESHOLD_5 - ADC_TOLERANCE) && 
        adc_val < (BTN_THRESHOLD_5 + ADC_TOLERANCE)) {
        return BTN_SCALE;
    }
    
    return 0;
}

// ============================================================================
// FUNCIÓN: ProcessButtons - CORREGIDA con manejo de overflow
// ============================================================================
void ProcessButtons(void) {
    int8 btn_func = ReadButtonAN2();
    int8 btn_ctrl = ReadButtonAN3();
    int8 current_btn = btn_func | btn_ctrl;
    
    // Detectar long-press (con manejo de overflow del timer)
    if (current_btn != 0 && current_btn == last_button) {
        unsigned int32 elapsed = get_ticks() - button_press_time;
        // Verificar que no haya overflow (elapsed < 2^31)
        if (elapsed > LONG_PRESS_MS && elapsed < 0x80000000) {
            if (system_state == STATE_IDLE && current_btn > 0x04) {
                RemoveFromQueue(current_btn);
                last_button = 0;
            }
            return;
        }
    } else {
        last_button = current_btn;
        button_press_time = get_ticks();
    }
    
    // Start/Stop tiene prioridad
    if (btn_ctrl == BTN_START) {
        delay_ms(200);
        
        if (system_state == STATE_IDLE) {
            if (queue_size > 0) {
                system_state = STATE_RUNNING;
                queue_index = 0;
                ClearResults();
                lcd_gotoxy(1, 4);
                printf(lcd_putc, "Ejecutando...     ");
                ProcessQueue();
            }
        } else {
            fputc(CMD_STOP, DSP_UART);
            ReturnToIdle();
        }
        return;
    }
    
    // En RUNNING, ignorar todo lo demás
    if (system_state == STATE_RUNNING) {
        return;
    }
    
    // Botón Clear
    if (btn_ctrl == BTN_CLEAR) {
        delay_ms(200);
        ClearQueue();
        ClearResults();
        current_page = 0;
        total_pages = 0;
        lcd_gotoxy(1, 4);
        printf(lcd_putc, "Cola limpiada     ");
        UpdateLEDs();
        return;
    }
    
    // Botón Load Select
    if (btn_ctrl == BTN_LOAD) {
        delay_ms(200);
        load_mode = 1 - load_mode;
        SetLoad(load_mode);
        UpdateLEDs();
        lcd_gotoxy(1, 4);
        if (load_mode == 0) {
            printf(lcd_putc, "Carga: 8 Ohm      ");
        } else {
            printf(lcd_putc, "Carga: 4 Ohm      ");
        }
        return;
    }
    
    // Botón Scale
    if (btn_ctrl == BTN_SCALE) {
        delay_ms(200);
        scale_mode++;
        if (scale_mode > 3) scale_mode = 0;
        
        if (scale_mode == 0) {
            auto_range = 1;
            lcd_gotoxy(1, 4);
            printf(lcd_putc, "Escala: AUTO      ");
        } else {
            auto_range = 0;
            SetScale(scale_mode);
            lcd_gotoxy(1, 4);
            if (scale_mode == 1) {
                printf(lcd_putc, "Escala: 1V        ");
            } else if (scale_mode == 2) {
                printf(lcd_putc, "Escala: 10V       ");
            } else {
                printf(lcd_putc, "Escala: 100V      ");
            }
        }
        return;
    }
    
    // Botones de función
    if (btn_func != 0) {
        delay_ms(200);
        
        if (btn_func == BTN_TS || btn_func == BTN_DDS) {
            ClearQueue();
            queue[0] = btn_func;
            queue_size = 1;
        } else {
            int1 already_in_queue = 0;
            int8 i;
            for (i = 0; i < queue_size; i++) {
                if (queue[i] == btn_func) {
                    already_in_queue = 1;
                    break;
                }
            }
            
            if (!already_in_queue && queue_size < MAX_QUEUE_SIZE) {
                queue[queue_size++] = btn_func;
            }
        }
        
        UpdateLEDs();
        lcd_gotoxy(1, 4);
        printf(lcd_putc, "Cola: %u pruebas    ", queue_size);
    }
}

// ============================================================================
// FUNCIÓN: RemoveFromQueue
// ============================================================================
void RemoveFromQueue(int8 test_id) {
    int8 found = 0;
    int8 i, j;
    
    for (i = 0; i < queue_size; i++) {
        if (queue[i] == test_id) {
            found = 1;
            for (j = i; j < queue_size - 1; j++) {
                queue[j] = queue[j + 1];
            }
            queue_size--;
            break;
        }
    }
    
    if (found) {
        UpdateLEDs();
        lcd_gotoxy(1, 4);
        printf(lcd_putc, "Quitado de cola   ");
    }
}

// ============================================================================
// FUNCIÓN: ReadEncoder
// ============================================================================
void ReadEncoder(void) {
    int8 state = 0;
    
    if (!input(ENC_A)) state |= 0x01;
    if (!input(ENC_B)) state |= 0x02;
    if (!input(ENC_BTN)) state |= 0x04;
    
    if (state != enc_last_state) {
        if ((enc_last_state == 0x00 && state == 0x01) ||
            (enc_last_state == 0x01 && state == 0x03) ||
            (enc_last_state == 0x03 && state == 0x02) ||
            (enc_last_state == 0x02 && state == 0x00)) {
            enc_direction = 1;
        }
        else if ((enc_last_state == 0x00 && state == 0x02) ||
                 (enc_last_state == 0x02 && state == 0x03) ||
                 (enc_last_state == 0x03 && state == 0x01) ||
                 (enc_last_state == 0x01 && state == 0x00)) {
            enc_direction = 2;
        }
        
        if (state & 0x04) {
            enc_btn_pressed = 1;
        }
        
        enc_last_state = state;
    }
}

// ============================================================================
// FUNCIÓN: ProcessQueue
// ============================================================================
void ProcessQueue(void) {
    for (queue_index = 0; queue_index < queue_size; queue_index++) {
        int8 test_id = queue[queue_index];
        
        lcd_gotoxy(1, 4);
        printf(lcd_putc, "Prueba %u/%u: ", queue_index + 1, queue_size);
        
        switch (test_id) {
            case ID_THD:
                printf(lcd_putc, "THD");
                break;
            case ID_SLEW:
                printf(lcd_putc, "SLEW");
                break;
            case ID_POWER:
                printf(lcd_putc, "POWER");
                break;
            case ID_TS_PARAMS:
                printf(lcd_putc, "T/S");
                break;
            case ID_FREQ_RESP:
                printf(lcd_putc, "FRESP");
                break;
            case 0x90:
                printf(lcd_putc, "DDS");
                break;
        }
        
        ExecuteTest(test_id);
        
        if (system_state != STATE_RUNNING) {
            break;
        }
    }
    
    if (queue_index >= queue_size) {
        ReturnToIdle();
    }
}

// ============================================================================
// FUNCIÓN: ExecuteTest
// ============================================================================
void ExecuteTest(int8 test_id) {
    int8 config[8];
    int8 checksum;
    
    if (test_id == ID_THD) {
        int16 muestras = 4800;
        int16 freq = (int16)thd_freq;
        
        config[0] = (muestras >> 8) & 0xFF;
        config[1] = muestras & 0xFF;
        config[2] = (freq >> 8) & 0xFF;
        config[3] = freq & 0xFF;
        config[4] = thd_mode;
        config[5] = 0;
        config[6] = 0;
        config[7] = 0;
        
        checksum = ComputeChecksum(config, 8);
        SendCommand(CMD_UPDATE_CONFIG, &test_id, 1);
        SendCommand(CMD_UPDATE_CONFIG, config, 9);
        
    } else if (test_id == ID_SLEW || test_id == ID_POWER) {
        int32 muestras = 512;
        float carga = 8.0;
        
        config[0] = (muestras >> 24) & 0xFF;
        config[1] = (muestras >> 16) & 0xFF;
        config[2] = (muestras >> 8) & 0xFF;
        config[3] = muestras & 0xFF;
        
        // Copiar float a bytes
        int8 i;
        int8 *carga_ptr = (int8*)&carga;
        for (i = 0; i < 4; i++) {
            config[4 + i] = carga_ptr[i];
        }
        
        checksum = ComputeChecksum(config, 8);
        SendCommand(CMD_UPDATE_CONFIG, &test_id, 1);
        SendCommand(CMD_UPDATE_CONFIG, config, 9);
    }
    
    SendCommand(CMD_EXECUTE, &test_id, 1);
    
    if (!WaitForACK(UART_TIMEOUT_MS)) {
        lcd_gotoxy(1, 4);
        printf(lcd_putc, "Error: Timeout    ");
        return;
    }
    
    SendCommand(CMD_READ_RESULT, &test_id, 1);
    
    unsigned int16 timeout = UART_TIMEOUT_MS;
    while (timeout-- && !line_ready) {
        delay_ms(1);
        CheckTemperature();
    }
    
    if (line_ready) {
        ParseAndStoreResult(rx_buffer);
        line_ready = 0;
        
        total_pages = result_count;
        if (current_page >= total_pages) {
            current_page = total_pages - 1;
        }
        
        DisplayCurrentPage();
    }
}

// ============================================================================
// FUNCIÓN: SendCommand
// ============================================================================
void SendCommand(int8 cmd, int8 *params, int8 len) {
    disable_interrupts(INT_RDA);
    
    fputc(cmd, DSP_UART);
    int8 i;
    for (i = 0; i < len; i++) {
        fputc(params[i], DSP_UART);
    }
    
    enable_interrupts(INT_RDA);
}

// ============================================================================
// FUNCIÓN: WaitForACK
// ============================================================================
int1 WaitForACK(unsigned int16 timeout_ms) {
    unsigned int16 timeout = timeout_ms;
    
    while (timeout--) {
        if (kbhit(DSP_UART)) {
            char resp = fgetc(DSP_UART);
            if (resp == ACK) return 1;
        }
        delay_ms(1);
    }
    return 0;
}

// ============================================================================
// FUNCIÓN: ParseAndStoreResult
// ============================================================================
void ParseAndStoreResult(char *rx_buf) {
    int8 start = 0;
    int8 count = 0;
    int8 i = 0;
    int8 str_idx = 0;
    char num_str[20];
    float values[10];
    int8 test_id = 0;

    // Verificar "RES:" manualmente
    if (rx_buf[0] != 'R' || rx_buf[1] != 'E' || rx_buf[2] != 'S' || rx_buf[3] != ':') return;

    start = 4;
    // Buscar primer ':'
    while (rx_buf[start] != ':' && rx_buf[start] != '\0') start++;
    if (rx_buf[start] == '\0') return;
    start++;

    // Parsear ID
    while (rx_buf[start] >= '0' && rx_buf[start] <= '9') {
        test_id = test_id * 10 + (rx_buf[start] - '0');
        start++;
    }

    // Buscar siguiente ':'
    while (rx_buf[start] != ':' && rx_buf[start] != '\0') start++;
    if (rx_buf[start] == '\0') return;
    start++;

    // Parsear valores separados por coma
    while (rx_buf[start] != '\0' && count < 10) {
        str_idx = 0;
        while (rx_buf[start] != ',' && rx_buf[start] != '\0' && str_idx < 19) {
            num_str[str_idx++] = rx_buf[start++];
        }
        num_str[str_idx] = '\0';

        // Convertir string a float manualmente
        float result = 0.0;
        float decimal = 0.1;
        int1 is_negative = 0;
        int8 idx = 0;
        
        if (num_str[idx] == '-') {
            is_negative = 1;
            idx++;
        }
        
        while (num_str[idx] >= '0' && num_str[idx] <= '9') {
            result = result * 10.0 + (num_str[idx] - '0');
            idx++;
        }
        
        if (num_str[idx] == '.') {
            idx++;
            while (num_str[idx] >= '0' && num_str[idx] <= '9') {
                result += (num_str[idx] - '0') * decimal;
                decimal *= 0.1;
                idx++;
            }
        }
        
        if (is_negative) result = -result;
        values[count] = result;
        count++;

        if (rx_buf[start] == ',') start++;
    }

    // Guardar en buffer
    if (result_count < MAX_RESULTS) {
        result_buffer[result_count].test_id = test_id;
        result_buffer[result_count].num_values = count;
        for (i = 0; i < count && i < 10; i++) {
            result_buffer[result_count].values[i] = values[i];
        }
        result_buffer[result_count].completed = 1;
        result_count++;
    }
}

// ============================================================================
// FUNCIÓN: DisplayCurrentPage
// ============================================================================
void DisplayCurrentPage(void) {
    if (total_pages == 0 || current_page >= result_count) {
        lcd_gotoxy(1, 1);
        printf(lcd_putc, "Sin resultados    ");
        lcd_gotoxy(1, 2);
        printf(lcd_putc, "                  ");
        lcd_gotoxy(1, 3);
        printf(lcd_putc, "                  ");
        lcd_gotoxy(1, 4);
        printf(lcd_putc, "Encoder: navegar  ");
        return;
    }
    
    TestResult_t *result = &result_buffer[current_page];
    
    lcd_gotoxy(1, 1);
    lcd_gotoxy(1, 2);
    lcd_gotoxy(1, 3);
    lcd_gotoxy(1, 4);
    
    switch (result->test_id) {
        case ID_THD:
            if (thd_mode == THD_MODE_IMD) {
                printf(lcd_putc, "IMD:  %f%%       ", result->values[0]);
                printf(lcd_putc, "F1:   %fVrms     ", result->values[1]);
                printf(lcd_putc, "F2:   %fVrms     ", result->values[2]);
                printf(lcd_putc, "SMPTE 60+7kHz       ");
            } else {
                printf(lcd_putc, "THD:%f%% N:%f%%", result->values[0], result->values[1]);
                printf(lcd_putc, "SINAD:%fdB SNR:%f", result->values[2], result->values[3]);
                printf(lcd_putc, "Fund:%fVrms       ", result->values[4]);
                printf(lcd_putc, "Freq:%fHz M:", thd_freq);
                if (thd_mode == 0) {
                    printf(lcd_putc, "Goertzel");
                } else {
                    printf(lcd_putc, "Residual");
                }
            }
            break;
            
        case ID_SLEW:
            printf(lcd_putc, "Slew Rate:          ");
            printf(lcd_putc, " %f V/us        ", result->values[0]);
            printf(lcd_putc, "Vmax: %fV        ", result->values[2]);
            printf(lcd_putc, "Freq:1000Hz Sq      ");
            break;
            
        case ID_POWER:
            if (current_page % 2 == 0) {
                printf(lcd_putc, "Power:%fW        ", result->values[0]);
                printf(lcd_putc, "Peak: %fW        ", result->values[1]);
                printf(lcd_putc, "Eff:  %f%%        ", result->values[5]);
                printf(lcd_putc, "Vrail:%fV        ", result->values[2]);
            } else {
                printf(lcd_putc, "Damp: %f         ", result->values[6]);
                printf(lcd_putc, "Zout: %f Ohm     ", result->values[7]);
                printf(lcd_putc, "VA:   %f VA      ", result->values[8]);
                printf(lcd_putc, "PF:   %f         ", result->values[9]);
            }
            break;
            
        case ID_TS_PARAMS:
            if (current_page % 2 == 0) {
                printf(lcd_putc, "Thiele-Small        ");
                printf(lcd_putc, "Fs:   %f Hz      ", result->values[0]);
                printf(lcd_putc, "Qts:  %f         ", result->values[1]);
                printf(lcd_putc, "Qes:  %f         ", result->values[2]);
            } else {
                printf(lcd_putc, "Thiele-Small        ");
                printf(lcd_putc, "Qms:  %f         ", result->values[3]);
                printf(lcd_putc, "Re:   %f Ohm     ", result->values[4]);
                printf(lcd_putc, "Zmax: %f Ohm     ", result->values[5]);
            }
            break;
            
        case ID_FREQ_RESP:
            if (current_page == 0) {
                printf(lcd_putc, "Freq Response       ");
                printf(lcd_putc, "Points: 32          ");
                printf(lcd_putc, "Noise:%f dB      ", result->values[32]);
                printf(lcd_putc, "DynSNR:%f dB     ", result->values[33]);
            } else {
                int8 freq_idx = (current_page - 1) * 4;
                if (freq_idx < 32) {
                    printf(lcd_putc, "%fHz:%f dB     ", 
                           (float)freq_idx, result->values[freq_idx]);
                }
                if (freq_idx + 1 < 32) {
                    printf(lcd_putc, "%fHz:%f dB     ", 
                           (float)(freq_idx+1), result->values[freq_idx+1]);
                }
                if (freq_idx + 2 < 32) {
                    printf(lcd_putc, "%fHz:%f dB     ", 
                           (float)(freq_idx+2), result->values[freq_idx+2]);
                }
                if (freq_idx + 3 < 32) {
                    printf(lcd_putc, "%fHz:%f dB     ", 
                           (float)(freq_idx+3), result->values[freq_idx+3]);
                }
            }
            break;
    }
    
    if (total_pages > 1) {
        lcd_gotoxy(16, 4);
        printf(lcd_putc, "%u/%u", current_page + 1, total_pages);
    }
}

// ============================================================================
// FUNCIÓN: UpdateLCD
// ============================================================================
void UpdateLCD(void) {
    if (system_state == STATE_IDLE) {
        lcd_gotoxy(1, 1);
        printf(lcd_putc, "RatNET Analyzer   ");
        lcd_gotoxy(1, 2);
        printf(lcd_putc, "THD Freq: %f Hz  ", thd_freq);
        lcd_gotoxy(1, 3);
        if (auto_range) {
            printf(lcd_putc, "Scale:AUTO Load:");
        } else {
            printf(lcd_putc, "Scale:    Load:");
        }
        if (load_mode == 0) {
            printf(lcd_putc, "8");
        } else {
            printf(lcd_putc, "4");
        }
        printf(lcd_putc, "Ohm             ");
        lcd_gotoxy(1, 4);
        printf(lcd_putc, "Queue: %u tests     ", queue_size);
    }
}

// ============================================================================
// FUNCIÓN: CheckTemperature
// ============================================================================
void CheckTemperature(void) {
    set_adc_channel(ADC_TEMP);
    delay_us(20);
    int16 temp_adc = read_adc();
    
    if (temp_adc > TEMP_MAX_ADC) {
        lcd_gotoxy(1, 4);
        printf(lcd_putc, "ALERTA TEMP!      ");
        
        SetRelay(RELAY_LOAD1, 0);
        SetRelay(RELAY_LOAD2, 0);
        
        if (system_state == STATE_RUNNING) {
            fputc(CMD_STOP, DSP_UART);
            ReturnToIdle();
        }
        
        delay_ms(2000);
    }
}

// ============================================================================
// FUNCIÓN: AutoRange
// ============================================================================
void AutoRange(void) {
    if (!auto_range) return;
    
    set_adc_channel(ADC_AUTORANGE);
    delay_us(20);
    int16 adc_val = read_adc();
    
    if (adc_val < 20) {
        SetScale(1);
    } else if (adc_val < 200) {
        SetScale(2);
    } else {
        SetScale(3);
    }
}

// ============================================================================
// FUNCIÓN: ComputeChecksum
// ============================================================================
int8 ComputeChecksum(int8 *data, int8 len) {
    int8 checksum = 0;
    int8 i;
    for (i = 0; i < len; i++) {
        checksum ^= data[i];
    }
    return checksum;
}

// ============================================================================
// FUNCIÓN: ClearResults
// ============================================================================
void ClearResults(void) {
    result_count = 0;
    current_page = 0;
    total_pages = 0;
}

// ============================================================================
// FUNCIÓN: ClearQueue
// ============================================================================
void ClearQueue(void) {
    queue_size = 0;
    queue_index = 0;
}

// ============================================================================
// FUNCIÓN: ReturnToIdle
// ============================================================================
void ReturnToIdle(void) {
    system_state = STATE_IDLE;
    queue_index = 0;
    UpdateLCD();
}

// ============================================================================
// FUNCIÓN: InitSystem
// ============================================================================
void InitSystem(void) {
    setup_adc_ports(AN0_TO_AN3);
    setup_adc(ADC_CLOCK_DIV_64);
    
    output_low(RELAY_MAX264);
    output_low(RELAY_TS);
    output_low(RELAY_LOAD1);
    output_low(RELAY_SCALE100V);
    output_low(RELAY_SCALE1V);
    output_low(RELAY_SCALE10V);
    output_low(RELAY_LOAD2);
    
    output_high(LCD_BL);
    
    lcd_init();
    lcd_gotoxy(1, 1);
    printf(lcd_putc, "RatNET Analyzer");
    lcd_gotoxy(1, 2);
    printf(lcd_putc, "Iniciando...");
    delay_ms(1000);
    
    ShiftRegister_Write(0x00);
    
    SetLoad(0);
    auto_range = 1;
    
    enable_interrupts(INT_RDA);
    enable_interrupts(GLOBAL);
    
    lcd_putc('\f');
    UpdateLCD();
}

// ============================================================================
// FUNCIÓN PRINCIPAL - CORREGIDA con manejo de overflow
// ============================================================================
void main(void) {
    InitSystem();
    
    while (TRUE) {
        ProcessButtons();
        
        // Encoder (con manejo de overflow)
        unsigned int32 enc_elapsed = get_ticks() - last_enc_read;
        if (enc_elapsed > ENC_DEBOUNCE_MS && enc_elapsed < 0x80000000) {
            ReadEncoder();
            last_enc_read = get_ticks();
            
            if (enc_direction == 1 && current_page < total_pages - 1) {
                current_page++;
                DisplayCurrentPage();
            } else if (enc_direction == 2 && current_page > 0) {
                current_page--;
                DisplayCurrentPage();
            }
            
            enc_direction = 0;
        }
        
        // Procesar respuesta UART
        if (line_ready) {
            ParseAndStoreResult(rx_buffer);
            line_ready = 0;
            
            if (result_count > 0) {
                total_pages = result_count;
                DisplayCurrentPage();
            }
        }
        
        // Temperatura (con manejo de overflow)
        unsigned int32 temp_elapsed = get_ticks() - last_temp_check;
        if (temp_elapsed > 1000 && temp_elapsed < 0x80000000) {
            CheckTemperature();
            last_temp_check = get_ticks();
        }
        
        // Autorango
        if (auto_range && system_state == STATE_IDLE) {
            AutoRange();
        }
        
        delay_ms(10);
    }
}
 
Atrás
Arriba