Pendientes y hallazgos abiertos¶
Documento vivo. Cada entrada tiene un dueño y una condición de cierre; nada se cierra sin evidencia. Los hallazgos marcados como inferencia salen del análisis del plan original y están pendientes de confirmación por los especialistas de dominio.
Hallazgos que hay que resolver antes de comprar hardware¶
Estos dos afectan a qué hardware hay que comprar. Ninguno cuesta más de una tarde de análisis ahora, y los dos cuestan mucho después.
P-01 · Tolerancia del tamaño de celda¶
Estado: cerrado, 7 de septiembre de 2026 — exactitud requerida, confirmado también para el modo fino de 125 m · Dueño: especialistas de dominio · Cerraba: D-04 y la arquitectura del DDC — ver ADR-001.
A 250 MSPS cada muestra cubre c/2 × 4 ns = 0,5996 m:
- 125 m → 208,48 muestras → R = 208 da 124,71 m; R = 209 da 125,31 m
- 250 m → 416,95 muestras → R = 417 da 250,03 m
No hay R entero exacto, y además R(250 m) ≠ 2 × R(125 m) si se busca el más cercano en cada caso (208 frente a 417). Importa porque la arquitectura natural es una cadena CIC/FIR donde el modo de 250 m añade una etapa ÷2 sobre el de 125 m.
Pregunta a responder: ¿el producto ORPG admite un error de celda del 0,24 %, o exige 125/250 m exactos? Si exige exactitud hay que ir a remuestreo fraccionario (Farrow) o a otra fs, y eso cambia el presupuesto de DSPs y la arquitectura completa.
Condición de cierre: respuesta escrita de los especialistas, y si es «exactitud requerida», un ADR nuevo que sustituya a D-04. A la luz del resultado de agosto de 2026, el ADR que hace falta no es de exactitud fraccionaria sino de elección de R sobre la rejilla de cuantización del campo.
Brief de investigación: research/p-01-orpg-gate-spacing.md (en el repositorio, no publicado en
este sitio) descompone la pregunta en cuatro sub-preguntas respondibles desde el código de ORPG y
del legacy RDA_Backend_Py, y corrige la cuantificación del error: lo que decide no es el error por
celda (0,29 m) sino el acumulado a 460 km, ~1,1 km.
Resultado, 30 de agosto de 2026: dos lecturas del árbol de fuentes real de ORPG build 24
(rpg_b24_0r1_20_pub_src, paquete CODE del ROC), hechas en la máquina que lo tiene montado, y
recogidas en research/p-01-RESULTADO.md y research/p-01-RESULTADO-barrido.md (en el
repositorio, no publicados en este sitio). En resumen:
- El espaciado es un campo transmitido, entero de 16 bits en metros. 124,71 m y 249,41 m no son declarables: se declaran como 125/124 y 249. La conversión de punto flotante a entero ocurre en nuestro firmware, no en ORPG.
- ORPG re-deriva el rango con una tabla cableada a 250 m y la aplica a todo lo que no declare exactamente 1000 m de celda. Declarar 249 cae en esa ruta con la corrección mal alineada.
- Hay tope duro de 1840 gates de reflectividad (1840 × 250 m = 460 km: la rejilla nativa de ORPG es de 250 m) y de 1200 en Doppler y dual-pol. Se recorta con aviso en el log, no se rechaza el radial.
- Por tanto el modo de 125 m con 3689 bins no llega entero a ORPG en ningún caso, y la elección de R₁ pasa a fijarla el consumidor interno.
- ORPG no tiene una banda de tolerancia: tiene una lista blanca. La recombinación azimutal de
super-res admite
{250, 1000}en reflectividad y solo 250 en Doppler, y descarta los dos radiales si el valor no está en la lista. La validación de moments dual-pol exige 250 exacto y rechaza el campo con error si no. Es decir, declarar 249 no degrada el producto: lo elimina. - El tope Doppler de 1200 gates (300 km a 250 m) no es un problema: el plan de la ZU9 pide 150 km por defecto y 230 km extendido, o sea 600 y 920 gates. Pero el modo Doppler tiene que declarar 250 igual que reflectividad, así que no puede usar una cadena de diezmado distinta.
La recomendación que sale de ahí es R₂ = 417 (250,026 m, que se declara como 250 exactos y da 1840 bins justos) con cadena propia, frente a R₂ = 416 (249,41 m, que se declara como 249 y da 1844 bins, cuatro por encima del tope).
Por qué el resultado de agosto no cerraba P-01 todavía
El árbol de ORPG no está en la máquina de desarrollo, así que ninguna de las citas
fichero:línea era reverificable ahí; el barrido sí reverificó las cuatro citas decisivas del
primer resultado y coinciden. Siguen sin revisar la composición geográfica entre radares, el
VAD/VWP y el remuestreo final a rejilla Level-III —los tres que el brief marcaba como daño
potencial número uno— y RDA_Backend_Py, la única fuente empírica, sigue sin localizar (ver
P-05). Esa parte de la verificación queda como deuda abierta aparte, no como condición
de cierre de P-01 (ver el cierre de abajo).
Lo que sí faltaba en agosto para cerrar P-01 era el modo fino: la evidencia de ORPG cubría el mecanismo de lista blanca y el caso concreto de 249 m, pero el modo de 125 m con 3689 bins no llegaba entero a ORPG en ningún caso, así que no estaba confirmado si el mismo criterio de exactitud aplicaba ahí.
Cierre, 7 de septiembre de 2026
Confirmado directamente por los especialistas de dominio: se exige exactitud también en el modo de 125 m, no solo en el de 250 m — mismo mecanismo de lista blanca que el resultado de agosto ya había identificado, ahora confirmado en el modo que quedaba abierto.
Nota de procedencia: esta respuesta llegó verbalmente en sesión (vladimir, 7-sep-2026), a
diferencia de research/p-01-RESULTADO.md y research/p-01-RESULTADO-barrido.md, que citan
fichero y línea del árbol de ORPG. No contradice esa evidencia — la extiende al modo fino, que
quedaba fuera de lo citado — pero no hay todavía un registro escrito citable para esta
pieza en concreto. Si aparece (correo, acta, ticket), añadirlo aquí.
Con esto, P-01 dejó de bloquear. La propuesta técnica que ya estaba escrita para esta respuesta
exacta — ADR-001, fs = 143,9 MHz con R₁ = 120 / R₂ = 240, que declara
125 m y 250 m exactos en las dos plataformas — sigue sin firmar: le falta el visto bueno de
dirección técnica y de compra de hardware (P-08/P-03), que P-01 nunca cubrió. config/params.toml
sigue sin tocarse hasta que esa tabla de firmas se complete.
P-02 · Capa física de los encoders SSI¶
Estado: abierto · Dueño: compra de hardware + especialistas · Bloquea: especificación de la portadora ZU9.
El plan original dice que «los dos encoders SSI son accesibles» pero nunca dice a través de qué. SSI en instrumentación industrial es habitualmente RS-422 diferencial, no LVCMOS. Si la portadora ZU9 no lleva transceptores RS-422, falta hardware.
Condición de cierre: confirmación documental de (a) la capa física real de los encoders del radar y (b) la presencia de transceptores RS-422 en la portadora candidata. Entrada D6 del registro delta hasta entonces.
P-03 · Retirada temprana del riesgo JESD204B¶
Estado: abierto · Dueño: dirección técnica · Impacto: es el riesgo número uno del proyecto.
Como la ZU9 + FMC213 aún no está comprada hay una ventana que probablemente no vuelva: la arquitectura de hardware todavía es una decisión abierta, no una restricción.
La ZedBoard no compra ni un punto porcentual de este riesgo: el XC7Z020-CLG484 no expone transceptores gigabit y su FMC es LPC. Una plataforma con MGTs y FMC HPC de segunda mano — ZC706 con XC7Z045, o ZCU104/ZCU102 en la familia UltraScale+ — permite hacer el spike de JESD204B + clocking + SYSREF con un FMC de ADC modesto, muy por debajo del coste del hardware final.
Descubrir a los seis meses que el árbol de clocking elegido no da latencia determinista es el escenario que arruina el mes 8.
Condición de cierre: decisión explícita de comprar o no comprar una plataforma de spike, con fecha.
Dato nuevo, 7-sep-2026: referencia externa disciplinada por GPS
Confirmado en sesión (vladimir): todo el sistema usa un reloj externo basado en GPS. Esto no cierra P-03 — sigue siendo el riesgo número uno, sin plataforma de spike — pero sí acota el árbol de clocking: no es un OCXO libre a bordo de la portadora, es una referencia externa disciplinada por GPS, casi con certeza la misma "referencia de 10 MHz de la casa" que ADR-001 ya asumía para el PLL del FMC213 (PFD de 100 kHz, N = 1439). Falta todavía la ficha técnica: formato de señal, conector, y si hay redundancia. Añadido como A9 a los supuestos de entorno.
Precisión, 7-sep-2026: son 10 MHz + 1PPS, y la referencia es local a este radar — no hay sincronización entre sitios de la red LAMULA que resolver aquí. Eso simplifica el problema: lo que el 1PPS compra en un radar único es UTC absoluto para el reloj de muestreo y, potencialmente, para el marcado temporal de los rayos en Z4 — no coherencia entre unidades. Con un solo radar, el argumento central de P-03 no cambia: sigue faltando la plataforma de spike para el propio enlace JESD204B, independientemente de la referencia.
P-04 · Sectorización azimutal de PRF y ancho de pulso («pie-slice»)¶
Estado: cerrado, 21-sep-2026 — confirmado, pero con una forma más fina que la enunciada originalmente · Dueño: vladimir · Cerraba: el alcance de Z2 — ver D-13.
El plan original nombra un único par PRF/ancho de pulso global por barrido. El estudio de la interfaz de anfitrión de un procesador de señal maduro sugiere una capacidad más granular: una tabla de valores de PRF y ancho de pulso que conmutan por sector azimutal dentro de un mismo barrido.
Si el checklist DSP↔RCP confirma que las estrategias de barrido del radar Gematronik objetivo lo necesitan, es un requisito del motor de timing en PL — las tablas de trigger necesitarían entradas por sector — y un campo correspondiente en la dirección de configuración del contrato DRx↔DSP. No se asume necesario sin esa confirmación.
21-sep-2026: confirmado — no es sectorización por azimut, es Batch mode de NEXRAD, 2 lotes por radial
La confirmación no vino del checklist DSP↔RCP sino de una decisión de producto directa (vladimir): el VCP objetivo usa Batch mode de NEXRAD, no pie-slice. La granularidad real es más fina que "por sector de varios grados": Batch mode alterna PRF dentro del dwell time de cada radial (~10-15 ms) — un lote corto de PRF baja (vigilancia, pocos pulsos) seguido de un lote de PRF alta (Doppler, muchos pulsos), en bucle continuo durante los 360° del barrido a esa elevación. El enunciado original de este pendiente (tabla de N entradas por sector azimutal) no es el mecanismo correcto; el mecanismo correcto es una tabla de exactamente 2 lotes por radial — fijado en D-13. Detalle de diseño y consecuencias para el motor de timing y para el validador PRF×rango de D-09, en D-13 y en Fases, Z2.
Pendientes de procedencia y de fuentes¶
Estos no son hallazgos técnicos del receptor; son huecos en la trazabilidad que bloquean o encarecen el trabajo de arriba.
P-05 · RDA_Backend_Py sin localizar¶
Estado: abierto · Dueño: vladimir · Bloquea: la parte empírica de P-01.
lamula-rcp/spike-fase0/README-rda-orpg.md documenta un proyecto legacy de 2013,
RDA_Backend_Py, que hizo ingesta en producción de los radares cubanos al ORPG citando el ICD
2620002 como fuente. Es la única fuente empírica disponible sobre qué acepta un ORPG real, y ya hay
precedente de que gana al ICD: RESULTADO-rda-orpg.md registra un caso en el que la dirección real
del loopback del legacy contradecía la lectura literal de las tablas, y se resolvió a favor del
legacy porque había corrido en producción.
No está en /home/node/workspace. Sin él, la investigación de P-01 solo puede leer
especificación, y el precedente del loopback dice que eso no basta.
Condición de cierre: ruta al repositorio o copia del código, anotada aquí.
P-06 · Dos copias vivas del project plan de la ZU9 — cerrado¶
Estado: cerrado el 30 de agosto de 2026 · Dueño: equipo.
El plan ZU9 existe en dos sitios publicados: docs/referencia/project-plan-zu9.md en este
repositorio y docs/drx-plan.md en lamula-dsp, que sigue en el nav de ese sitio. El fuente del
plan de ZedBoard (lamula-drx-zedboard-plan.md) sí se eliminó de lamula-dsp al escindir este
repositorio, pero el del plan ZU9 no.
Mientras las dos copias existan, un cambio en una no se refleja en la otra. La opción de retirar
drx-plan.md del sitio DSP se planteó y se decidió no tocarlo por ahora, porque rompe enlaces
existentes a ese sitio.
Resolución: las dos cosas. La copia canónica es la de este repositorio,
docs/referencia/project-plan-zu9.md —que además es la que tiene los enlaces a algorithms/
reescritos como URL absolutas al sitio DSP, así que no depende de dónde se publique—, y
lamula-dsp/docs/drx-plan.md pasa a ser una página de redirección que apunta aquí. La página se
conserva en vez de borrarse para no romper los enlaces existentes, y la entrada del nav de aquel
sitio dice «(movido al sitio del DRx)» para que se vea antes de hacer clic. Empujado y desplegado el mismo día: los dos sitios sirven ya
la versión nueva.
P-07 · Conformidad con la rejilla de NEXRAD — trasladado al RCP¶
Estado: trasladado el 30 de agosto de 2026 · Dueño: proyecto RCP.
Salió del barrido de P-01: si ORPG valida el espaciado de celda contra una lista cerrada, es
razonable esperar que valide igual el resto de la geometría del radial —conteo de gates, rango al
primer gate, resolución azimutal, tamaño de palabra de dato— y data_word_size ya está confirmado
en la misma condición de código que rechaza el espaciado.
No es un pendiente de este proyecto. El DRx no habla con ORPG; su única interfaz externa es
DRx↔DSP. Y el DSP tampoco: lamula-dsp/docs/dsp-plan.md:71 retira el contrato DSP↔ORPG, y el
RCP es quien posee la emulación RDA WSR-88D (ICD 2620002) y el feed por radial. Vive ahora como
PEND-RCP-12 en lamula-rcp/docs/alcance/pendientes.md.
Lo único que llega al DRx de todo esto es el tamaño de celda, que es P-01, y la elección de fs,
que es P-08.
P-08 · fs es un grado de libertad, y bien elegida disuelve P-01¶
Estado: abierto, con una propuesta concreta y sin datos externos pendientes · Dueño: dirección técnica + compra · Bloquea: el cierre de D-04.
Los convertidores del FMC213 son de 250 MSPS, pero nada obliga a operarlos a 250 MHz. El tamaño
de celda es R · c / (2·fs): si fs es libre, se elige fs para que la celda caiga donde se
quiere, en vez de elegir R para que caiga cerca.
Precisión sobre el límite matemático. Celda de 125 m exacta al metro exige
fs = R · c/250 = R · 1,199169832 MHz, y ese cuanto no cae nunca sobre una rejilla redonda: para
que fs fuera un número entero de MHz haría falta que R fuera múltiplo de 125 000 000. No se
puede tener celda exacta y reloj redondo a la vez. Lo que sí se puede es un reloj redondo con un
error tres órdenes de magnitud menor que el actual, que a efectos prácticos es lo mismo.
La propuesta: fs = 143,9 MHz, R₁ = 120, R₂ = 240¶
| R | celda | error acumulado a 460 km | |
|---|---|---|---|
| Modo fino | 120 | 125,000330 m | 1,2 m sobre 3680 bins |
| Modo grueso | 240 | 250,000660 m | 1,2 m sobre 1840 bins |
Contra los 1054 m de D-04 tal como está hoy. 1,2 m es el 0,5 % de una celda: irrelevante para cualquier producto, y el valor declarado a ORPG sigue siendo 250 exacto, que es lo que exige la lista blanca de P-01.
Lo que trae de regalo:
- Reloj redondo. 143,9 MHz está en la rejilla de 100 kHz, así que sale por integer-N desde
la referencia de 10 MHz de la casa (PFD 100 kHz, N = 1439). Sin PLL fraccional-N, sin espurios
fraccionales — que era la única objeción seria a la variante de
fsexacta. - Factores de diezmado mucho mejores. 240 = 2⁴·3·5 y 120 = 2³·3·5, frente a 416 = 2⁵·13 y 208 = 2⁴·13. El 13 de D-04 es un factor incómodo para una cadena CIC; 240 y 120 se descomponen en etapas cómodas.
- R₂ = 2·R₁ intacto: se conserva la cadena única con etapa ÷2. Una sola cadena de diezmado, no dos.
- Menos presión de recursos y de timing. ~~Y en ZedBoard el factor de dilatación baja de 2,5 a
1,439.~~ Corregido el 30 de agosto de 2026: eso exigiría un
fsde ZedBoard de 100 MHz, y conif_norm= 600/1439 los 100 MHz son ilegales —el IF no caería en hercios enteros, que es una condición que comprueba el paquete de parámetros en las dos plataformas. El valor legal más cercano es 99,998988 MHz, 10,1 ppm por debajo, y con él la dilatación es 1,439015. Ver ADR-001.
Alternativas evaluadas¶
fs |
R₁ / R₂ | celda de 250 m | acumulado a 460 km | nota |
|---|---|---|---|---|
| 143,9 MHz | 120 / 240 | 250,000660 m | 1,2 m | integer-N, factores limpios |
| 145,1 MHz | 121 / 242 | 249,999224 m | 1,4 m | R = 2·11² , peor para CIC |
| 149,9 MHz | 125 / 250 | 249,993711 m | 11,6 m | R muy limpio (2·5³) |
| 153,5 MHz | 128 / 256 | 249,989802 m | 18,8 m | R potencia de dos |
| 247,0 MHz | 206 / 412 | 250,029337 m | 54,0 m | el mejor en rejilla de 1 MHz |
| 249,427325 MHz | 208 / 416 | 250,000000 m | 0 m | exacto, pero pide fraccional-N o cristal a medida |
| 250 MHz, R₂ = 417 | — / 417 | 250,026910 m | 49,5 m | Opción B de P-01: obliga a dos cadenas |
| 250 MHz, R₂ = 416 (D-04 hoy) | 208 / 416 | 249,427325 m | 1054 m | se declara 249: ORPG descarta el radial |
Supuestos que hay que verificar antes del ADR¶
Ninguno se puede comprobar desde este repositorio:
- ~~Rate mínimo del convertidor del FMC213.~~ Resuelto el 30 de agosto de 2026: 143,9 MHz está dentro de especificación, con margen. Ver el análisis debajo de esta lista.
- ~~Plan de frecuencia intermedia.~~ Resuelto el 30 de agosto de 2026: el IF es de 60 MHz, como ya decía contexto. Encaja — ver el análisis debajo de esta lista.
- JESD204B. La tasa de línea baja con
fs, lo que va a favor, pero hay que confirmar que el modo del convertidor admite ese rate y que SYSREF sigue siendo submúltiplo entero del LMFC. - Ancho de banda. Con pulsos de 0,8 / 1,66 / 3,3 µs el ancho de banda es de ~1,25 MHz como máximo, así que no es restricción a ninguna de estas frecuencias.
El convertidor admite 143,9 MHz, y en el modo de dos lanes¶
El ADC del FMC213 son dos TI ADS42JB69 —dual, 16 bits, 250 MSPS, JESD204B— que dan los cuatro canales de la tarjeta; el DAC es el AD9129 que ya nombra el plan. La tabla de condiciones recomendadas del ADS42JB49/JB69 (SLAS900F, §7.3) especifica el rate de muestreo con mínimo y máximo, y depende del modo de serialización:
| Modo SERDES | LMFS | Lanes por ADC | Rate de muestreo | Línea a 143,9 MHz |
|---|---|---|---|---|
| 10x | L=4, M=2, F=1, S=1 | 2 | 60 – 250 MSPS | 1,439 Gbps |
| 20x | L=2, M=2, F=2, S=1 | 1 | 40 – 156,25 MSPS | 2,878 Gbps |
143,9 MHz cae dentro de los dos, y no marginalmente: son 2,4 veces el mínimo del modo 10x. El
riesgo que esta lista marcaba como «el real» de bajar a 143,9 MHz no existe. Los máximos de
línea del propio convertidor son 2,5 Gbps en 10x y 3,125 Gbps en 20x, y la especificación de
tiempos de subida cubre 600 Mbps ≤ bit rate ≤ 3,125 Gbps, así que las dos líneas de la tabla
están dentro.
Y trae una consecuencia que no estaba en el análisis anterior: a 250 MHz el modo 20x no es utilizable —su máximo son 156,25 MSPS— y hay que ir a 10x, o sea 4 lanes por ADC, 8 transceptores para los cuatro canales. A 143,9 MHz los dos modos están disponibles, y el 20x hace la tarjeta entera con 4 transceptores a 2,878 Gbps. Es la mitad de MGTs en la portadora ZU9, y afecta a la selección de la portadora en P-03.
Lo que queda por confirmar, y no es bloqueante para el ADR:
- Que el PLL del FMC213 sintetiza 143,9 MHz. VadaTech describe un «wide-band PLL» con referencia por panel frontal, portadora FMC o referencia de a bordo, pero no publica la pieza en la ficha pública (la hoja de datos del FMC213 devuelve HTTP 403). La nota de aplicación del ADC usa un LMK04828 como ejemplo; si el FMC213 lleva ese, 143,9 MHz sale con VCO a 2446,3 MHz (= 143,9 × 17, dentro del rango de VCO0) y N entero desde 10 MHz con PFD de 100 kHz. La objeción de ruido de fase se desplaza aquí: PFD de 100 kHz sube el ruido en banda del PLL2 frente a un PFD de 10 MHz, y el presupuesto de jitter es de ~100 fs rms. Hay que pedir a VadaTech la pieza del PLL y su configuración.
- SYSREF. El ADC exige
f_SYSREF = f_clk / (n · K · F)con1 < K < 32y K > 16 recomendado. A 143,9 MHz en modo 20x (F = 2) con K = 32: 2,2484 MHz. Divisor entero desde el reloj de dispositivo, sin fracciones. No hay problema aritmético a ninguna de lasfscandidatas. - Jitter de apertura. El ADS42JB69 especifica 85 fs rms, coherente con el análisis de IF de arriba.
El IF de 60 MHz encaja en 143,9 MHz sin muestreo subnyquist¶
Con fs = 143,9 MHz la mitad de Nyquist queda en 71,95 MHz, así que los 60 MHz del IF caen en la
primera zona de Nyquist: se muestrea directo, sin submuestreo y sin inversión espectral.
- Banda de señal: 59,375 – 60,625 MHz, con el pulso más corto (0,8 µs, ~1,25 MHz de ancho).
- Filtro antialias: lo que pliega sobre 60 MHz es la energía en
fs − IF= 83,9 MHz. El filtro tiene que pasar hasta 60,6 y rechazar en 83,9 — una razón de transición de 1,38, cómoda para un paso-banda de IF convencional. - Armónicos del IF: el segundo (120 MHz) pliega a 23,9 MHz y el tercero (180 MHz) a 36,1 MHz. Ninguno cae sobre la banda útil.
- Jitter de apertura: a 60 MHz, 100 fs rms limitan la SNR a 88,5 dB, y 200 fs a 82,5 dB. No es restrictivo, pero es la razón por la que conviene no subir el IF.
- NCO: 60 / 143,9 = 0,416956 de
fs. Es una palabra de fase cualquiera para el DDS Compiler, sin nada especial.
Lo que se descarta a propósito: poner el IF en fs/4 para tener un mezclador sin
multiplicadores exigiría fs = 240 MHz exactos, y ahí la mejor celda es 249,827 m — 318 m de error
acumulado, más de una celda entera desplazada a 460 km. No compensa: por
D-06 el mezclador sale del DDS Compiler más un multiplicador complejo, que en
esta pieza es barato.
Impacto en el RTL: ninguno por el lado de fs, si se cumple D-02 — todo
se deriva de FS_HZ. Sí cambian R₁ y R₂, que son parámetros de la cadena de diezmado, no
arquitectura.
Condición de cierre: un ADR que sustituya a D-04 fijando fs, R₁ y R₂ juntos. Escrito el 30
de agosto de 2026 y pendiente de firma: ADR-001. El rate mínimo
del convertidor ya está confirmado y no se opone, así que no queda ningún dato externo pendiente
para escribirlo. Los planes de repliegue siguen documentados por si el PLL del FMC213 no sintetiza
143,9 MHz: 247 MHz con R₁ = 206 / R₂ = 412 (54 m de error, reloj en rejilla de 1 MHz, cadena única)
y, en último caso, la Opción B de P-01.
P-09 · cocotb no tiene backend para xsim, y D-05 lo daba por hecho¶
Estado: decidido el 30 de agosto de 2026, primera ejecución real el 5 de septiembre de
2026 (máquina de desarrollo con Vivado, no el runner de CI de A7), los tres bugs de
infraestructura encontrados en esa ejecución ya corregidos y reconfirmados el mismo día, sobre
xsim y sobre Verilator 5.006 · Dueño: equipo · Cierre pendiente de que Z1 escriba
drx_ddc_top; la infraestructura de L1 ya no tiene bugs conocidos.
Estado, 6/7-sep-2026: drx_ddc_top escrito Y EJECUTADO contra IP real — ver P-10
Esta nota decía "no ejecutado" porque se escribió antes de descubrir que esta misma máquina de
desarrollo sí tiene Vivado 2022.2 y Verilator 5.006 instalados (solo faltaban en PATH). Ya
corrió de verdad, varias veces, contra IP real generada con tcl/gen_ip.tcl y simulada con
tcl/sim_l1.tcl sobre xsim. El detalle completo — tres bugs reales encontrados y corregidos, y
uno que sigue abierto — está en P-10.
D-05 dice que un único runner de cocotb cubre los dos backends, Verilator para
L0 y xsim para L1. Verilator sí; xsim no. Los runners que trae cocotb 1.9.2 son icarus,
questa, ghdl, nvc, riviera, verilator y xcelium. No hay xsim, y no es una omisión de la
versión pinada: cocotb nunca ha soportado el simulador de Vivado.
Esto importa porque L1 es exactamente el nivel que valida lo que Verilator no puede elaborar: la IP del DDC. Sin L1 no hay comparación bit a bit contra el DDS/CIC/FIR Compiler reales, que es el hito ZM1 entero.
Opciones, ordenadas por coste creciente. La 1 es la adoptada:
- Testbenches SystemVerilog nativos para xsim en L1, fuera de cocotb. ← adoptada El oráculo sigue siendo el modelo dorado: se le hace escribir vectores de entrada y de salida esperada a fichero, y el testbench de xsim los lee y compara. Se pierde el «un solo runner» de D-05 y se gana no depender de ninguna licencia nueva.
- Licencia de Questa o Riviera. cocotb los soporta y elaboran IP de Xilinx. Coste de licencia por puesto, y hay que verificar que las bibliotecas compiladas de Xilinx están disponibles para esa versión.
- Netlist funcional post-síntesis simulada en Verilator. Vivado puede exportar la IP como netlist; Verilator podría elaborarla. Requiere Vivado igualmente, es lento y el resultado ya no es la IP sino su síntesis. Poco atractivo.
- Escribir un backend de xsim para cocotb. Semanas de trabajo sobre la interfaz VPI de xsim, fuera del alcance de este proyecto.
Decisión: la opción 1. L1 sale de cocotb y pasa a testbenches nativos de SystemVerilog sobre xsim, con el modelo dorado como oráculo cruzando por fichero. La enmienda está escrita en D-05. Lo construido el mismo día:
| Pieza | Qué hace | ¿Verificado? |
|---|---|---|
model/drx_model/l1_vectors.py |
Exporta estímulo y salida esperada en $readmemh, con manifiesto y SHA-256 |
Sí, en make check |
tools/gen_l1_vectors.py |
Genera los casos de las dos plataformas y los dos modos | Sí |
sim/tb_sv/drx_l1_file_source.sv |
Lee un .hex y lo entrega como stream con handshake |
Sí, sobre Verilator 5.006 (reconfirmado el 5-sep-2026 tras el cambio de interfaz a KIND) y sobre xsim |
sim/tb_sv/drx_l1_checker.sv |
Compara bit a bit contra el modelo y cuenta discrepancias | Sí, sobre Verilator |
sim/tb_sv/tb_l1_harness.sv + sim/tests/test_l1_harness.py |
Auto-prueba del arnés: cero discrepancias con vectores limpios, exactamente una con un bit cambiado | Sí |
sim/tb_sv/tb_l1_ddc.sv |
El testbench de ZM1 | Lint sí; no elabora: drx_ddc_top es de Z1 |
tcl/sim_l1.tcl |
Flujo xsim: manifiesto, IP de los XCI, plusargs | Ejecutado el 5-sep-2026; ver hallazgo debajo |
El orden importa: el comparador está probado antes de que exista el DUT, y probado en la dirección que engaña —un comparador que no compara da verde con cualquier cosa—, así que el día que Z1 conecte el DDC, un verde de L1 significa algo.
sim/runner.py se queda siendo el runner de L0 y su backend L1 sigue fallando explícito, ahora
apuntando al flujo correcto en vez de a un bloqueo.
Hallazgo, 5 de septiembre de 2026: primera ejecución real, tres bugs de infraestructura, los tres corregidos¶
Primera vez con Vivado disponible (máquina de desarrollo, no CI). tcl/sim_l1.tcl nunca había
corrido, y no corrió limpio a la primera: la primera ejecución real encontró tres defectos propios
del arnés de simulación, ninguno relacionado con que drx_ddc_top todavía no exista.
create_project -in_memoryno soportalaunch_simulation. Falla conERROR: [Common 17-53] User Exception: No open project.tcl/build.tclsí puede usar-in_memoryporquesynth_design/write_bitstreamno lo necesitan, pero el flujo de simulación de xsim sí escribe estructura de proyecto en disco. Corregido: proyecto real (create_project sim_l1 -part $part -force) dentro de$opt(outdir), que ya es directorio de build — no se rompe D-07.- Los plusargs se pasaban como tokens
+clave=valorsueltos. El parser de opciones de xsim los trata como posicionales y rompe a partir del séptimo (ERROR: [Common 17-165] Too many positional options when parsing '+mode=0');xsim --helpdocumenta que hace falta anteponer-testplusarga cada uno. Corregido entcl/sim_l1.tcl.
Con los dos fixes, la elaboración funciona —compila y elabora sin drx_ddc_top, exactamente
como está diseñado para hacerlo en esta fase— pero run all revelaba un tercer problema: xsim
abortaba con
FATAL_ERROR: Vivado Simulator kernel has discovered an exceptional condition from which it cannot
recover. Process will terminate. [...]
Time: 0 ps Iteration: 0 Process: /tb_l1_ddc/u_stimulus/Initial46_1
File: sim/tb_sv/drx_l1_file_source.sv
No era uno de los tres $fatal propios del código (ninguno de esos mensajes aparecía en el log):
era un choque interno del kernel de xsim.
$value$plusargscon unparameter stringcomo formato revienta el kernel de xsim.drx_l1_file_source.svparametrizaba el formato completo (PATH_FMT/COUNT_FMT, p. ej."stimulus_path=%s") para poder reutilizar el módulo con distintas claves de plusarg entb_l1_harness.sv(expected_i_path/expected_q_path). xsim 2022.2 no tolera que ese argumento de formato sea un parámetro — aunque sea constante de elaboración — y revienta el kernel en vez de dar un error de Tcl o de SV. Aislado en 6 líneas fuera del repo: el mismo$value$plusargs("stimulus_path=%s", path)funciona con el literal escrito a mano y revienta si ese literal llega por unparameter string; confirmado con Vivado 2022.2 en esta máquina. No hay AR de Xilinx encontrado que lo documente; es un hallazgo propio, no una incompatibilidad conocida y publicada.
Corregido: se quitaron PATH_FMT/COUNT_FMT. En su lugar, un parameter string KIND
—cerrado a los tres usos reales ("stimulus", "expected_i", "expected_q")— selecciona en
runtime, con un case, entre llamadas a $value$plusargs que llevan el literal escrito a mano
en cada rama. Aislado el mismo patrón con dos instancias de distinto KIND en el mismo
top: ambas resuelven bien, sin crash. Aplicado a sim/tb_sv/drx_l1_file_source.sv y a sus dos
sitios de instanciación (tb_l1_ddc.sv, tb_l1_harness.sv).
Con los tres fixes, tcl/sim_l1.tcl corre de punta a punta por primera vez sin ningún ERROR ni
FATAL_ERROR de xsim: compila, elabora, y run all llega exactamente al comportamiento diseñado
para esta fase —el testbench aborta porque falta drx_ddc_top (trabajo de Z1), y como el DUT
nunca produce muestras, la red de seguridad de timeout de tb_l1_ddc.sv también dispara a los 40 ms
como está previsto—. EXIT_CODE=0 del propio vivado -mode batch: los $fatal de SystemVerilog no
se propagan como error de Tcl, así que ese código no certifica por sí solo que L1 pasó; lo que
certifica el diseño correcto de la fase es la ausencia de FATAL_ERROR/ERROR y la presencia
exacta de Fatal: tb_l1_ddc: falta drx_ddc_top... en el log.
El cambio de interfaz de drx_l1_file_source.sv (parámetro KIND en vez de
PATH_FMT/COUNT_FMT) se reconfirmó corriendo sim/tests/test_l1_harness.py sobre Verilator
5.006 —la versión que ya tenía verificado el proyecto, compilada desde fuente porque el
apt install verilator de Ubuntu 22.04 solo trae la 4.038, demasiado vieja para --timing—: los
dos casos (vectores limpios y con un bit corrupto) siguen en verde, igual que toda sim/tests
completa. P-09 no tiene ningún bug conocido pendiente; lo único que falta para el hito ZM1 es
que Z1 escriba drx_ddc_top.
19-sep-2026: launch_simulation reutilizó un kernel compilado de antes de D-12 — dos casos de la matriz dieron falso-fallo
Al correr la matriz completa de 16 combinaciones bajo el comparador con tolerancia (D-12),
tono_sobre_escala__fine y tono_sobre_escala__coarse fallaron con conteos de discrepancias
altos (222/315 y 103/157) pese a que las muestras impresas mostraban residuos de solo 3-4 LSB,
dentro de la banda ya aceptada. La causa: build/sim/xsim/zedboard/tono_sobre_escala__*/ traía
un directorio de simulación de una corrida del 18-sep (antes de que existiera
TOLERANCE_LSB) — elaborate.log mostraba drx_l1_checker sin el sufijo (TOLERANCE_LSB=5)
que sí aparecía en los otros 14 casos, y su fecha de modificación era literalmente del día
anterior. launch_simulation/xelab decidió que no había nada que recompilar y reutilizó el
kernel viejo, con el parámetro en su valor por defecto (0, bit a bit estricto) — silencioso, sin
ningún aviso de "usando snapshot en caché" que lo delatara aparte de la ausencia del sufijo en
el log. Corregido borrando esos dos directorios de build y build/l1-vectors/ (el sha de
parámetros también había quedado desactualizado por el cambio de fs_hz de la misma sesión,
ver Fases, Z1) y corriendo limpio: 16/16 "PASS", confirmado
con el (TOLERANCE_LSB=5) correcto en el log de elaboración de los dos casos. Mismo patrón de
hallazgo que el resto de P-09 —infraestructura de simulación, no del diseño— así que se
documenta aquí. Lección para la próxima vez que cambie algo en drx_l1_checker.sv o en
config/params.toml: borrar build/sim/xsim/ (o al menos el caso afectado) antes de confiar
en una corrida, no asumir que launch_simulation detecta el cambio.
P-10 · drx_ddc_top contra IP real: cuatro bugs corregidos¶
Estado: cerrado, 7 de septiembre de 2026 · Dueño: equipo. Queda un residuo nuevo y mucho
menor sin investigar (±1 LSB en tono_desplazado__fine, ver el cierre más abajo) — no bloquea Z1.
Primera ejecución real de tcl/gen_ip.tcl + tcl/sim_l1.tcl contra el DDC de un canal
(rtl/drx_ddc_top.sv, rtl/drx_ddc_mixer.sv, rtl/drx_round.sv, rtl/drx_saturate.sv), sobre
Vivado 2022.2 real en esta máquina de desarrollo (ver nota bajo P-09). Caso de aceptación:
tono_if__fine, 65536 muestras de estímulo, 315 muestras esperadas.
Corregido, los tres con causa raíz confirmada por simulación, no por conjetura:
- El DDS Compiler libre no entrega fase 0 en su primera muestra válida. Con el mezclador ya
corregido para no atar
nco_readya contrapresión (el NCO no congela su acumulador de fase aunque nadie consuma, así que contrapresurearlo pierde pasos de fase para siempre — ver el aviso de cabecera dedrx_ddc_mixer.sv), la primera muestra seguía sin ser(32767, 0). Búsqueda inversa exacta contra la LUT del propio modelo (Nco.lut()) identificó la muestra real como el índice 656 de esa tabla — un desfase de fase fijo y reproducible (igual conHas_TREADYque sin él, así que no es la FIFO de contrapresión: es intrínseco al arranque del Phase Generator). Corregido entcl/gen_ip.tcl:Has_TREADY {false}(el mezclador nunca contrapresura al NCO, así que esa FIFO no compraba nada) másPhase_offset {Fixed}conPOFF1calculado para cancelar el índice 656 en la propia IP — la corrección va en la fuente de la fase, no en un giro complejo aguas abajo (un giro sobre valores de LUT ya cuantizados no puede reconstruir el valor exacto de la LUT en el índice correcto; se probó y dejaba un sesgo de ±1 LSB en régimen permanente que no cerraba). - El transitorio de arranque del CIC no coincidía con el modelo, mucho más allá de lo que
explica un desfase de fase.
model/drx_model/cic.pycalculaba con latencia cero (recursión matemática instantánea); el CIC Compiler real es una tubería de N integradores + N peines con latencia de silicio de verdad. Ajuste numérico contra los valores reales de xsim: anteponer exactamenterate - 1muestras de relleno de tubería antes de que el modelo empiece a acumular reproduce el transitorio real bit a bit (residuo nulo salvo el hallazgo #3 de abajo). Corregido en el modelo dorado (Cic.pipeline_fill, cambio deliberado y autorizado, huellasGOLDENdemodel/tests/test_chain.pyactualizadas con la fecha y el motivo) — no en el RTL, porque el RTL ya se comporta como la IP real; era el modelo el que idealizaba la latencia. nco_readysin conexión tras quitarHas_TREADY:dds_compiler_ncoya no tiene puertom_axis_data_treadyuna vez generado sin TREADY.rtl/drx_ddc_top.svya no lo conecta; la salidanco_readydel mezclador (que sigue existiendo, atada a1'b1) queda explícitamente sin uso, igual ques_last.
Abierto — la hipótesis de amplitud del 6/7-sep se probó contra xsim real y quedó descartada:
Con los tres fixes de arriba, tono_if__fine pasó de 312-313/315 discrepancias (con errores de
hasta 5× en el transitorio) a 306/315, todas de ±1 LSB — el transitorio ya cierra bit a bit,
pero el residuo uniforme sigue sin explicación confirmada.
Hipótesis descartada, 7-sep-2026: no es Amplitude_Mode {Full_Range}
La primera lectura de este hallazgo (más abajo se conserva tachada) apuntaba a que el DDS
Compiler usa como pico 2^(N-1) - 2 en vez del máximo estándar de complemento a dos que asume
fixed.limits(). Se implementó (Nco.lut() con pico 2^15-2), se regeneraron los vectores L1
y se corrió tono_if__fine contra xsim real: el conteo de discrepancias no cambió ni una,
306/315 exactas, lo que ya era sospechoso — un cambio de escala de ~3·10⁻⁵ sobre valores de
salida de orden 10³ no puede mover ningún redondeo lo suficiente como para cerrar un residuo de
±1 LSB completo.
La prueba concluyente: tcl/gen_ip.tcl ya documentaba una medición real contra la IP,
LUT[656] = (32702, 2059), de la investigación del hallazgo #1. Recalculando esa misma entrada
en Python, el pico original del modelo (2^15-1 = 32767) reproduce (32702, 2059) exacto;
el pico "corregido" (2^15-2 = 32766) da (32701, 2059) — mal por 1 en el coseno, peor que
antes. El modelo original ya estaba en lo correcto en ese punto medido; el cambio se revirtió
íntegro (model/drx_model/nco.py, model/tests/test_bloques.py,
model/tests/test_chain.py) y make check vuelve a estar en 69/69 con las huellas GOLDEN
originales. La cita de pg141 sobre el pico de Full_Range describe el envolvente de diseño del
modo, no el factor de escala real de cada muestra de la LUT — no aplica aquí.
El residuo de ±1 LSB en tono_if__fine seguía sin causa conocida en este punto — que su
origen SÍ resultó ser el NCO, solo que no por amplitud, está documentado a continuación.
Localizado, 7-sep-2026: SÍ es el NCO — nco_sin sale con el signo cambiado
La instrumentación de abajo (revertida del árbol, no comiteada — sondas temporales en
sim/tb_sv/tb_l1_ddc.sv sobre g_branch[i].cic_rounded, fir_rounded, y dentro de
drx_ddc_mixer: pair_en/in_mem/nco_cos/nco_sin/prod_i/prod_q) localizó el origen
en tres pasadas:
- CIC → FIR: el sesgo (I: −1, Q: +1 respecto al modelo) ya está presente en
cic_roundeddesde la primera muestra en régimen (g_branch[0].cic_rounded), y el FIR lo propaga sin añadir nada nuevo — descarta CIC/FIR/drx_roundcomo origen, apunta aguas arriba, al mezclador o al NCO. - Mezclador → NCO:
prod_q(el producto crudo, sin redondear) del mezclador YA lleva el sesgo. Conx[n]conocido exacto (mismo estímulo), reconstruirnco_sinimplícito a partir deprod_q = −(x[n]·nco_sin)y compararlo contraNco.lut()da la respuesta:nco_sinreal coincide en magnitud con elsindel modelo pero con el signo invertido, muestra a muestra, desden=1(ejemplos: modelosin=32702→ IPsin=−32702; modelosin=−32186→ IPsin=+32186).nco_coscoincide con el modelo salvo un ruido de ±1-5 unidades — consistente con compararcos(θ)contracos(θ)evaluados por rutas de coma flotante ligeramente distintas (coses función par, así que el signo no lo delata), mientras quesin(θ)sí delata el signo invertido de inmediato (sines impar). PINC1no es la causa: se comparó el valor dePINC1ya escrito enip/dds_compiler_nco/dds_compiler_nco.xcicontraparams.nco_phase_incdel modelo — son bit a bit idénticos, no el complemento. El incremento de fase que de verdad usa la IP es el correcto; lo que sale invertido es el campo SINE del DDS Compiler para ese mismo incremento, no la velocidad ni el sentido del acumulador.
Lo que esto explica de una vez: por qué I cierra casi exacto (coseno es función par,
insensible a una inversión de signo en el seno) y por qué Q tiene un sesgo consistente pero
pequeño tras el CIC/FIR (un tono coherente que decima a un residuo cercano a cero sigue cerca
de cero incluso con el signo de la parte alterna invertido) — y por qué la hipótesis de
amplitud de arriba no tenía ningún efecto: el problema nunca fue de escala.
No corregido todavía — es una decisión, no un ajuste ciego. Dos caminos abiertos, ninguno aplicado:
- Ajustar el convenio de signo en el modelo dorado (
Nco.lut()/Ddc.mix(): usarq = +x·sin_wen vez de−x·sin_w, o negarsin_luten la propia tabla) — si el NCO real define "seno" con el convenio contrario al que asumemodel/drx_model/nco.py(p. ej. el DDS Compiler entrega el seno de la señal analíticae^{-jθ}en vez dee^{+jθ}, una convención de cuadratura distinta, no un bug). - O buscar la opción de configuración del DDS Compiler que invierte el signo (pg141, "Polarity
— Negative Sine";
tcl/gen_ip.tclno toca ese parámetro hoy, así que corre con el valor de fábrica) y corregirlo ahí si el convenio del modelo es el que se quiere conservar.
Para reproducir: las sondas usadas no están en el árbol (se revirtieron tras extraer los
datos). Recrearlas es sencillo: $display sobre
g_branch[0/1].cic_rounded/fir_rounded en tb_l1_ddc.sv, y dentro de drx_ddc_mixer,
sobre pair_en/in_mem[in_rd_ptr]/nco_cos/nco_sin/prod_i/prod_q (ver
rtl/drx_ddc_mixer.sv); comparar contra Nco.generate() y los productos crudos
x·cos_w/−x·sin_w calculados en Python para el mismo estímulo (tools/gen_l1_vectors.py,
caso tono_if, semilla SEED=20260830).
Decisión tomada y aplicada, 7-sep-2026: se ajusta el modelo, no la IP
Se decidió (vladimir, en sesión) conservar la configuración de la IP tal como está y ajustar el
modelo dorado para que coincida con el convenio de cuadratura real del DDS Compiler.
Cambiado: Ddc.mix() en model/drx_model/chain.py — q = shift_round(x·sin_w, shift) en
vez de shift_round(-x·sin_w, shift). Se actualizó también el comentario de cabecera de
rtl/drx_ddc_mixer.sv (documentación, no lógica: el RTL no cambia, porque prod_q <= -(...)
ya hacía lo correcto una vez que se entiende que nco_sin trae el signo de e^{+jwn}) y la
fórmula/snippet de docs/tutorial/03-ddc.md, que citaba el código viejo literalmente.
Validación contra xsim real, con los vectores L1 regenerados:
tono_if__fine(el caso de aceptación de arriba): sigue en 306/315, sin cambio. Esto no es que el fix no sirva — es que este caso concreto es degenerado para probarlo: el tono está exactamente en fase con el NCO, así queQconverge a ~0 en régimen permanente con cualquiera de las dos convenciones de signo (negar algo que ya es ~0 sigue siendo ~0). El mismo efecto ya se había visto enmodel/tests/test_chain.py: su huellaGOLDENtampoco se movió con el cambio, por la misma razón — verificado a mano, no es una casualidad sospechosa.tono_desplazado__fine(tono corrido del IF, sí sensible al signo deQ): 156/315 discrepancias, todas de ±1 LSB en un solo canal (ejemplos:dut=(-747,-4220) modelo=(-748,-4219),dut=(3692,2259) modelo=(3692,2258)) — nada parecido al sesgo sistemático y consistente de antes ((-1,+1)en las 306 muestras). Es la confirmación real: el fix de signo es correcto. El residuo de ±1 LSB que queda es de una magnitud completamente distinta — consistente con el ruido de punto flotante de ±1-5 unidades ya visto al compararnco_cosentre modelo e IP (arriba, hallazgo #2: evaluarcos/sinen direcciones espejadas por rutas de coma flotante distintas no da bit a bit lo mismo) — y queda como pendiente aparte, mucho menor, sin investigar todavía.make check: 69/69 con las huellasGOLDENdetono_ifsin cambiar (ver arriba) ymkdocs build --strictlimpio tras tocar el tutorial.
P-10 se da por cerrado en cuanto a los tres bugs de RTL/Tcl y el convenio de signo del
mezclador. Lo que queda (el ±1 LSB residual en tono_desplazado) es un hallazgo nuevo, mucho
más pequeño, y no bloquea Z1.
Investigación adicional del residuo, mismo día: se buscó una explicación barata (sin más corridas de Vivado) antes de decidir que no vale la pena perseguirlo más. Dos vías, ninguna dio una explicación limpia:
- Proximidad al borde del bin de LUT. Para las 14 primeras muestras de
tono_if(mismo estímulo, ya con datos capturados), la fracción de fase dentro del bin de LUT (16 bits por debajo de la dirección) no correlaciona con qué muestras coincidían exacto y cuáles no — hay coincidencias con la fase casi en el borde (frac=0,32) y discrepancias con la fase casi centrada (frac=0,56). Descarta un desajuste de redondeo de dirección por 1 unidad cerca de un borde de bin como explicación única. - Leer el contenido real de la LUT en vez de reconstruirlo por simulación. El modelo de
simulación del DDS Compiler (
ip/dds_compiler_nco/hdl/dds_compiler_v6_0_vh_rfs.vhd, ~27 000 líneas) es genérico — cubre todas las configuraciones posibles del core (Taylor, varios anchos, streaming/fixed) y genera la tabla algorítmicamente en tiempo de elaboración, no la trae como una constante literal de 65536 entradas para esta configuración concreta. Leerlo directamente no es un atajo; habría que volver a instrumentar xsim.
Decisión: no perseguirlo más por ahora. Es ruido de ±1 LSB sobre valores de 16 bits — está
en el suelo de cuantización del propio receptor (el tutorial de DDC ya mide un suelo de espurio
de −86,4 dBc para este NCO; un ULP de diferencia entre la tabla de numpy y la que genera
internamente el DDS Compiler es exactamente la clase de discrepancia que un suelo de ese orden
ya contempla). Seguir esta pista costaría otra ronda de sondas + xsim (~20 min) por un hallazgo
que no cambia ninguna decisión de arquitectura ni bloquea Z1. Si alguien lo retoma: instrumentar
nco_cos/nco_sin para las 56 direcciones de LUT que este tono visita en régimen permanente
(ya identificadas en la investigación de arriba) y comparar una a una contra Nco.lut() es el
siguiente paso más barato.
Matriz completa de los 6 casos de L1 contra xsim real, 7-sep-2026
Con tono_if__fine y tono_desplazado__fine ya cubiertos arriba, se corrieron los 4 casos que
faltaban (dos_tonos__fine y los tres *__coarse, que ejercitan la semibanda ÷2 de diezmado
grueso — no probada contra IP real hasta este punto). Resultado, con
n_expected = 315 en fino y 157 en grueso:
| Caso | Discrepancias | Patrón |
|---|---|---|
tono_if__fine |
306/315 (97 %) | Q≈0 en régimen permanente (degenerado); casi toda muestra difiere |
tono_desplazado__fine |
156/315 (50 %) | disperso, ±1 LSB, un canal por muestra |
dos_tonos__fine |
25/315 (8 %) | disperso, ±1 LSB, mayormente canal I |
tono_if__coarse |
147/157 (94 %) | casi constante, +1 en Q, I exacto (degenerado, igual que en fino) |
tono_desplazado__coarse |
44/157 (28 %) | disperso, ±1 LSB, ambos canales |
dos_tonos__coarse |
7/157 (4 %) | disperso, ±1 LSB, mayormente canal I |
Hipótesis de trabajo — no confirmada con instrumentación nueva: los 6 casos son
consistentes con una única causa, el mismo residuo de ±1 LSB de arriba. La tasa de
discrepancias correlaciona con qué tan degenerado es el caso: tono_if fuerza Q≈0 en régimen
permanente en los dos modos (fino y grueso), y ahí el mismo sesgo de redondeo del NCO se dispara
en casi el 100 % de las muestras; dos_tonos reparte la señal en un rango más amplio de
amplitud (los dos tonos de esa mezcla están a ±0,0008 en frecuencia normalizada del IF, cerca
pero no en el punto degenerado) y por eso cae a 4-8 %; tono_desplazado, con el rango más amplio
de los tres estímulos, queda en el medio (28-50 %). Ningún caso muestra la firma del bug de
signo ya corregido arriba (que era (-1,+1) simultáneo en ambos canales, no un residuo disperso
de un solo canal).
Esto es una lectura de los agregados, no una confirmación por sonda — el paso más barato
siguiente sigue siendo el mismo que se dejó anotado arriba: instrumentar nco_cos/nco_sin
muestra a muestra para las direcciones de LUT que cada caso visita y comparar contra
Nco.lut(). No se ha hecho todavía para estos 4 casos nuevos.
~~El residuo uniforme vendría de que el DDS Compiler en modo Amplitude_Mode {Full_Range} apunta a
un pico de 2^(N-1) - 2 (confirmado con el ejemplo numérico de pg141 v6.0 p.51: para 8 bits da
126 = 0b01111110, exactamente lo que dice el manual), mientras que fixed.limits() del modelo
asume el máximo estándar de complemento a dos, 2^(N-1) - 1.~~ Descartado arriba.
Cerrado, 7-sep-2026: el UNUSEDPARAM y un segundo bug encontrado al investigarlo
Con Vivado y Verilator ya en PATH en esta máquina, make check completo (el que corre los
dos simuladores, no solo make sim) reveló dos fallos reales, los dos ya corregidos:
- El
UNUSEDPARAMdertl/generated/drx_ddc_ip_pkg.svque hacía fallarsim/tests/test_l1_harness.pyytest_l0_heartbeat.pyal elaborar contra Verilator 5.006 era un falso positivo:drx_ddc_top.svsí usa cada uno de los anchos del paquete, víaimport drx_ddc_ip_pkg::*;, pero Verilator atribuye el warning a instancias que no lo importan (tb_l1_harness.u_checker) en vez de reconocer el uso real. Mismo patrón que los waivers ya existentes paradrx_ssa_pkg/drx_params_pkg— añadido ensim/verilator_waivers.vlt. fw/src/drx_params_selftest.cexigía contrato v0.1 — literal que se quedó sin tocar en los dos bumps deliberados y ya autorizados por D-08 (v0.2 entx_pol_v, v0.3 en el enum de canales), así quemake checkenfw/fallaba con «version del contrato DRx<->DSP inesperada» desde el 3-sep-2026 sin que nadie lo notara — ninguno de esos dos commits corrió elmake checkde la raíz. Corregido a v0.3.
Con los dos fixes, make check vuelve a estar en verde, 69/69. Sin relación con el hallazgo de
amplitud de arriba.
8-sep-2026: el residuo escala con la ganancia — replicación a 4 canales + escalado de ganancia
Z1 sumó dos piezas nuevas: rtl/drx_ddc_quad_top.sv (réplica de drx_ddc_top a los N_RX_CH
canales, para el hito ZM1) y la etapa de ganancia fina en drx_ddc_top.sv
(FINE_GAIN_COEFF/COARSE_GAIN_COEFF, generadas por tools/gen_params.py desde
fixed.gain_trim, que ya trae el oráculo desde el 7-sep). Las dos se verificaron matemáticamente
correctas contra el modelo (mismo coeficiente, mismo algoritmo de redondeo) y contra xsim real.
Al correr tcl/sim_l1_quad.tcl, el canal 0 (tono_if__fine) reporta 306/315
discrepancias — el mismo número, exacto, que ya documenta la tabla de arriba para el caso de
un solo canal. No es un bug nuevo: es el residuo de ±1 LSB del NCO (ya aparcado arriba, «no
perseguirlo más por ahora») escalado junto con el resto de la cadena por el nuevo multiplicador
de ganancia — de ~1 LSB a ~2-3 LSB en valor absoluto, proporcionalmente igual en dB por debajo de
fondo de escala. Es la consecuencia esperada de escalar la señal a fondo de escala, no una señal
de que la réplica a 4 canales o el ajuste de ganancia tengan un error propio.
Confirmado por aislamiento: corriendo tcl/sim_l1.tcl contra el commit 6b32e99 (el que
documenta la tabla de arriba) en un worktree separado, con vectores regenerados desde cero, se
reproduce el mismo 306/315 — la tabla de esta página ya reflejaba este residuo, no una
ejecución limpia; el título del commit («completar la matriz») describe haber terminado de
correr y documentar los 6 casos, no haberlos dejado en verde.
Consecuencia para ZM1: el hito («coincide bit a bit… en los cuatro canales») sigue sin
cerrarse — igual que ya estaba antes de esta sesión — por este mismo residuo preexistente, no por
la réplica a 4 canales en sí. rtl/drx_ddc_quad_top.sv y la etapa de ganancia fina quedan
escritos, revisados y verificados en su propio alcance; cerrar ZM1 de verdad sigue pendiente de
retomar la investigación del NCO que esta página ya deja anotada (instrumentar nco_cos/
nco_sin muestra a muestra contra Nco.lut()), decisión que se dejó explícitamente para más
adelante.
8-sep-2026: instrumentado por fin — el residuo es del propio NCO, ±5 LSB crudos, y no depende de Memory_Type
Se retomó la instrumentación que esta página dejaba anotada como «el siguiente paso más barato»:
sonda temporal en sim/tb_sv/tb_l1_ddc.sv ($fwrite sobre u_dut.nco_cos/u_dut.nco_sin en
cada ciclo con nco_valid, sin decimar, indexado por n=0,1,2,…; revertida del árbol tras
extraer los datos, no comiteada) contra tono_if__fine en xsim real. Con phase_inc =
1030792151 el paso de dirección de LUT por muestra es de ~15732/65536 — casi ninguna dirección
se repite en 4096 muestras, así que esto cubre muchas más direcciones que las ~56 del régimen
permanente decimado.
Resultado, 4096 muestras crudas contra Nco.generate(4096): el signo invertido de Q ya
documentado y corregido en el modelo se confirma otra vez (no es nuevo). Después de compensar
ese signo, queda un residuo de hasta ±5 LSB en AMBOS canales, presente desde la muestra n=0
(ip=(32766,-2) vs modelo=(32767,0)) — no es un transitorio de arranque, es ruido por muestra
en la propia salida cruda del NCO, antes de cualquier CIC/FIR. Esto es MAYOR que el ±1 LSB medido
después de decimar — consistente con que CIC/FIR actúan de filtro paso bajo y promedian un ruido
no correlado de unos pocos LSB hasta dejarlo en ±1 LSB a la salida.
Corrección a una nota anterior de esta página: la entrada de arriba («Leer el contenido real
de la LUT…») decía que dds_compiler_v6_0_vh_rfs.vhd es «genérico, sin tabla literal» — eso era
una inferencia sobre el tamaño del fichero (~27 000 líneas), no una lectura real: el fichero está
cifrado con `protect (IEEE P1735, claves de Xilinx/Aldec/Mentor/Cadence/etc.), así que su
contenido no es legible sin las claves de descifrado que trae xsim. Esta página nunca tuvo forma
de confirmar por lectura de código cómo genera la IP sus valores; toda conclusión sobre su
arquitectura interna es inferencia por comportamiento, no lectura de fuente. Corregido aquí para
que nadie repita el intento de "leer el VHDL".
Hipótesis probada y descartada: Memory_Type. El .xci mostraba Memory_Type {Auto} (no
tocado hasta ahora); report_property -all confirma que los valores válidos son Auto,
Distributed_ROM, Block_ROM — es decir, "Auto" no garantiza necesariamente una ROM literal de
doble precisión ya redondeada como asume Nco.lut(). Se forzó CONFIG.Memory_Type {Block_ROM}
en tcl/gen_ip.tcl, se borró y regeneró ip/ completo (mismos anchos de TDATA en las cuatro
IP, confirmado), y se repitió la misma sonda de 4096 muestras: el resultado fue bit a bit
IDÉNTICO al de Auto (mismos nco_cos/nco_sin, mismo residuo de ±5 LSB, muestra a muestra).
Memory_Type gobierna solo el mapeo a recursos de FPGA en post-síntesis (BRAM vs ROM
distribuida); no cambia en nada el modelo de simulación comportamental de xsim. Cambio revertido
íntegro (git checkout -- tcl/gen_ip.tcl sim/tb_sv/tb_l1_ddc.sv, ip/ regenerado de vuelta a
Auto con tcl/gen_ip.tcl, árbol limpio confirmado con git status).
Dónde queda esto: Amplitude_Mode (descartado arriba), Noise_Shaping (ya en None, sin
tocar — dither, no relevante) y ahora Memory_Type son las tres hipótesis con una palanca de
configuración real que se han probado. Ninguna mueve el residuo. Sin acceso al código fuente del
core (cifrado) ni a la documentación de PG141 en este entorno, no hay más palancas conocidas que
probar sin especular. La lectura más simple de la evidencia: el core calcula
internamente su seno/coseno con un algoritmo propio de Xilinx que no coincide, LSB a LSB, con
round(2**15-1 · cos/sin(2π·addr/65536)) en punto flotante de 64 bits — una discrepancia
plausible entre dos implementaciones numéricas distintas del mismo cálculo, no una Amplitude_Mode
o modo de memoria mal elegido de nuestro lado. Esto es inferencia, no una causa raíz
confirmada por lectura de código — sigue sin haber una explicación de primeros principios de
por qué esos LSB concretos difieren.
Consecuencia práctica (superada por la entrada siguiente): con D-06 (Xilinx-IP-first)
vigente, no hay una palanca de configuración conocida para eliminar este residuo sin sustituir
el NCO por HDL propio. El residuo está acotado (±5 LSB crudos, ±1 LSB tras decimar, dentro del
suelo de -86,4 dBc ya medido) y es estable entre corridas y entre Memory_Type. La
recomendación de esta entrada era dejar de perseguir la causa raíz y redefinir el criterio "bit
a bit" del hito ZM1 — no se aplicó, y la entrada siguiente la deja obsoleta: Memory_Type
quedó descartado como palanca, pero no era la única disponible.
8-sep-2026: SÍ había una palanca — NCO_LUT_OFFSET_K estaba mal calibrado por una posición de LUT
El ajuste lineal del bloque anterior (error_cos ≈ +1,03e-4·sin θ, error_sin ≈ -1,15e-4·cos
θ, R²~0,8 en las dos ramas, mismo signo y orden de magnitud) es la firma de un desfase de
ÁNGULO sistemático, no de ruido aleatorio de redondeo — y su magnitud (~1,05e-4 rad) es casi
exactamente UN paso de dirección de LUT (2π/65536 ≈ 9,59e-5 rad). Eso apuntaba directamente a
NCO_LUT_OFFSET_K (tcl/gen_ip.tcl, hoy en 656): esa constante se calibró el 7-sep con una
sola muestra reconstruida a mano (LUT[656] = (32702, 2059)), suficiente para fijar el orden
de magnitud del desfase de arranque del Phase Generator pero sin margen para detectar un error
de una sola posición sobre esa única muestra — sobre todo si esa muestra en concreto tenía un
residuo cercano a 0 por casualidad (que pasa: varias de las 4096 muestras de la sonda lo tienen).
Confirmado contra xsim real, en las dos direcciones:
K=657(un paso más): el error post-hoc que hace falta para volver a alinear direcciones de LUT SUBE de +1 a +2 pasos — dirección equivocada, el residuo empeora (máximo ±8 LSB crudos).K=655(un paso menos): el error post-hoc que hace falta baja a 0 — dirección correcta. ConK=655la salida cruda del NCO (misma sonda de 4096 muestras) cae de máximo ±5 LSB a máximo ±2 LSB, con la mayoría de las muestras ya en ±1.- En el checker real, no solo en la sonda cruda: corriendo
tcl/sim_l1.tclcontratono_if__fineconK=655, el canalQen régimen permanente (el caso degenerado,Q≈0) pasa de un sesgo constante de+3LSB (conK=656) a 0 exacto, muestra tras muestra (dut=(11597,0) modelo=(11599,0), antesdut=(11597,3) modelo=(11599,0)). El canalImantiene un residuo constante de-2en este caso concreto, del mismo orden que el suelo de cuantización ya caracterizado — no se llegó a bit exacto, pero la mejora es real y medible en la comparación que de verdad importa (el checker de L1, no solo la sonda).
Estado del cambio: tcl/gen_ip.tcl y ip/dds_compiler_nco/dds_compiler_nco.xci ya reflejan
K=655, comiteado (7d38c3d). La validación era parcial en el momento del commit (sonda cruda
de 4096 muestras, ±5→±2 LSB, más un solo caso parcial) — la matriz completa ya se corrió, ver
la entrada siguiente para los números reales y la conclusión sobre ZM1.
~~Siguiente paso, pendiente: dejar correr tcl/sim_l1_quad.tcl (o los 6 casos de
tcl/sim_l1.tcl) hasta el final con K=655...~~ Hecho, ver abajo.
8-sep-2026: matriz completa con K=655 — mejora real y grande en la mayoría de los casos, pero ZM1 sigue sin cerrar bit a bit
Los 6 casos de tcl/sim_l1.tcl corridos hasta el final contra xsim real, K=655:
| Caso | K=656 (tabla original) |
K=655 |
|---|---|---|
tono_if__fine |
306/315 (97 %) | 303/315 (96 %) |
tono_desplazado__fine |
156/315 (50 %) | 48/315 (15 %) |
dos_tonos__fine |
25/315 (8 %) | 27/315 (9 %) |
tono_if__coarse |
147/157 (94 %) | 3/157 (2 %) |
tono_desplazado__coarse |
44/157 (28 %) | 18/157 (11 %) |
dos_tonos__coarse |
7/157 (4 %) | 7/157 (4 %) — sin cambio |
Lectura, separando lo confirmado de lo que queda abierto:
K=655sí es una corrección real y grande en los dos casostono_desplazadoy entono_if__coarse(caídas de 28-94 % a 2-15 %) — confirma que el desfase de fase de un paso de LUT era, en esos casos, el componente dominante del residuo.tono_if__finecasi no mejora (97 %→96 %) porque su residuo dominante no es de fase: es el-2LSB constante enIya visto en la validación parcial (dut=11597vsmodelo=11599, régimen permanente), que no depende deK—Qsí cerró exacto, pero el caso sigue casi 100 % discrepante porIsolo.dos_tonos__fineydos_tonos__coarseno cambian en absoluto (27 vs 25, 7 vs 7, dentro del ruido de muestreo o exactamente iguales) — esto es nuevo:Kno tiene ningún efecto medible ahí, lo que apunta a que estos dos casos concretos están dominados por un mecanismo de error distinto, todavía no identificado, que ni el bug de signo (cerrado hace días) ni el desfase deKexplican.
Conclusión: K=655 se queda (es estrictamente mejor o igual que K=656 en los 6 casos, nunca
peor) pero ZM1 no cierra — ningún caso llega a 0 discrepancias. Hay, como mínimo, dos
mecanismos de residuo sin explicar y sin relación conocida entre sí: el -2 LSB de I en
tono_if (visible también en grueso probablemente, no aislado todavía) y el que domina
dos_tonos en los dos modos. Seguir bajando esto necesitaría repetir la instrumentación de
nco_cos/nco_sin (la misma técnica que encontró el bug de K) pero específicamente para el
estímulo de dos_tonos, comparando bit a bit contra Nco.lut() en las direcciones de LUT que
ese estímulo concreto visita — no se ha hecho todavía.
Para ZM1: con esto, la decisión de redefinir (o no) el criterio "bit a bit" ya no es una
resignación sobre un residuo uniforme de ±1 LSB explicado por el suelo de cuantización del NCO
— es una decisión sobre si vale la pena seguir cazando un segundo mecanismo de error, distinto
del de K, que hoy deja entre el 2 % y el 96 % de discrepancias según el caso. Sigue pendiente
tener esa conversación con el equipo de DSP (qué tolera realmente su cadena de momentos frente a
un residuo de este orden — no documentado en este repo) antes de decidir si esto bloquea ZM1 o
si se acepta y se redefine el criterio.
8-sep-2026: el frontal analógico son 5 conversores, no 4+2 — corregido n_tx_burst_ch, contrato a v0.4
Detalle de hardware que no estaba documentado en ningún sitio de docs/alcance/ ni
docs/plataforma/fuente-de-muestras.md: el frontal analógico real tiene 5 conversores, no
la cuenta que asumía implícitamente el código (n_rx_ch=4 + n_tx_burst_ch=2 = 6 canales
reservados en el contrato). Confirmado en sesión: 2 conversores por polarización (H usa dos,
V usa dos — n_rx_ch=4 en sí ya era correcto, casualmente) más 1 solo conversor para la
muestra del transmisor, no dos.
Cambios aplicados:
config/params.toml:n_tx_burst_chde 2 a 1.contract/schema/drx_dsp_v0_1.toml: se eliminatx_burst_1del enumchannel;version_minorsube a 4 (compatible hacia atrás — ningún consumidor conocido usaba el bit 32 todavía).fw/src/drx_params_selftest.c: el literal de versión esperada, que ya mordió una vez (ver el hallazgo de arriba, 7-sep-2026), actualizado a 0.4 en el mismo commit que el bump — para no repetir el desfase.make gen+make check: 74/74 en verde.
Actualización, mismo día: confirmado en sesión qué distingue a los dos conversores de una
misma polarización: uno lleva la señal a ganancia nominal y el otro la lleva atenuada, para
extender el rango dinámico (no es diversidad ni un alimentador monopulso). El enum channel
del contrato ya documenta rx_0/rx_2 como nominal (H/V) y rx_1/rx_3 como atenuado (H/V) —
pero el emparejamiento H/V-nominal/atenuado es lo confirmado, no el orden concreto de bits
(¿cuál conversor físico es rx_0 frente a rx_1?): eso sigue siendo una convención asumida en
el schema, marcada como tal, pendiente de contrastar contra el cableado real.
Lo que queda abierto, sin confirmar en esta sesión:
- Capacidad de la plataforma ZU9 candidata. ADR-001 registra que cada
FMC213 lleva dos ADS42JB69 (2 boards × 2 chips × 2 canales = 8 canales de capacidad), así que 5
conversores caben con holgura — pero eso es una lectura de la hoja de datos de la portadora,
no una confirmación de que el mapeo de conectores real asigna H/V/Tx-sample a esa capacidad
como se espera. Sigue siendo hardware no comprado (ver cabecera de
CLAUDE.md). - Avisar al proyecto DSP. D-08/
docs/tutorial/05-contrato.mdson explícitos: «un contrato que cambia sin avisar no es un contrato». Este bump de v0.3 a v0.4 no se ha comunicado todavía fuera de este repositorio.
P-11 · Presupuesto de bits del NCO frente al rango dinámico intra-celda¶
Estado: cerrado, 19-sep-2026 — confirmado que el techo de Taylor alcanza (ver el cierre más abajo); el criterio de aceptación de L1 para este residuo queda fijado en D-12. Dueño: equipo (RTL) + especialistas de dominio · Cerraba: D-11 y el criterio de aceptación de L1 para el residuo del NCO.
18-sep-2026: Taylor pasa a ser el modo principal del NCO — decisión operativa, no cierre técnico (D-11)
Sin esperar el número real de supresión de clutter (sigue sin responder de especialistas, ver
"lo que falta" más abajo), se decide en sesión adoptar Noise_Shaping
{Taylor_Series_Corrected} como modo principal del NCO para el resto de las pruebas. La
justificación técnica es la que ya había dejado esta investigación: Taylor reduce el residuo
medido de ~5,7 a ~1,15 LSB rms sin costo de DSP48 relevante dado el presupuesto de 3 canales ya
cerrado en P-12 (159/220, 72 %), y su propio residuo de reproducción en el modelo dorado
(±2 LSB en 25-75 % de las muestras) es la misma clase que el proyecto ya acepta para None
desde P-10 — detalle completo, con las cifras, en D-11.
Esto no cierra P-11 en sentido estricto: sigue sin conocerse el número real de supresión de
clutter, así que sigue sin poder afirmarse que el techo de Taylor (~−88/−92 dBc) alcanza. Es una
decisión de avanzar con la mejor opción medida en vez de esperar esa respuesta — el mismo patrón
que ya usó la sesión del 9-sep-2026 para no bloquearse en el punto 1 de abajo. Tampoco cambia el
criterio de aceptación bit a bit de L1 (D-05): el residuo de Taylor se compara igual de estricto
que el de None, y ZM1 sigue sin cerrarse por el mismo motivo que documenta
Fases, Z1.
Migración ejecutada, 18-sep-2026 (misma sesión): ip/dds_compiler_nco.xci regenerado de
verdad contra Vivado 2022.2 real (4c7eae5, a7e5681) — Latency sube de 8 a 9,
C_NOISE_SHAPING de 0 a 2, exactamente lo que ya medían las sondas fuera del árbol de esta
entrada. Los otros tres XCI (CIC, FIR droop, FIR semibanda) regeneran byte a byte idénticos —
confirma que el cambio no tocó nada fuera del NCO. Sanidad contra xsim real, un caso
(tono_if__fine): 303/315 discrepancias (96 %), ±1-2 LSB — la misma clase y casi la misma
cifra que el modo None ya documentaba (306/315) para este mismo caso degenerado (P-10). No es
una sorpresa ni una regresión: confirma en hardware real lo que D-11 ya asumía del modelo.
Matriz completa, 18/19-sep-2026: las 16 combinaciones (8 casos × 2 modos) corridas contra xsim real, ~30 min por corrida en esta máquina. Lanzada desatendida, un caso a la vez, con cada fila comiteada y subida en cuanto terminaba esa corrida — conclusión completa después de la tabla.
| Caso | Discrepancias | Lectura |
|---|---|---|
tono_if__fine |
303/315 (96 %) | ±1-2 LSB, misma clase que None (P-10) |
tono_if__coarse |
3/157 (2 %) | ±0-5 LSB (primeras 16 impresas), misma clase que None (P-10) |
tono_desplazado__fine |
48/315 (15 %) | ±0-3 LSB (primeras 16 impresas), misma clase que None (P-10) |
tono_desplazado__coarse |
17/157 (11 %) | ±0-5 LSB (primeras 16 impresas), misma clase que None (P-10) |
dos_tonos__fine |
34/315 (11 %) | ±0-3 LSB (primeras 16 impresas), misma clase que None (P-10) |
dos_tonos__coarse |
9/157 (6 %) | ±0-5 LSB (primeras 16 impresas), misma clase que None (P-10) |
tono_desplazado_neg__fine |
62/315 (20 %) | ±0-3 LSB (primeras 16 impresas), misma clase que None (P-10) |
tono_desplazado_neg__coarse |
24/157 (15 %) | ±0-5 LSB (primeras 16 impresas), misma clase que None (P-10) |
tono_con_ruido__fine |
91/315 (29 %) | ±0-3 LSB (primeras 16 impresas), misma clase que None (P-10) |
tono_con_ruido__coarse |
29/157 (18 %) | ±0-5 LSB (primeras 16 impresas), misma clase que None (P-10) |
tono_con_offset_dc__fine |
0/315 | exacto bit a bit |
tono_con_offset_dc__coarse |
0/157 | exacto bit a bit |
tono_desbalanceado__fine |
87/315 (28 %) | ±0-3 LSB (primeras 16 impresas), misma clase que None (P-10) |
tono_desbalanceado__coarse |
30/157 (19 %) | ±0-5 LSB (primeras 16 impresas), misma clase que None (P-10) |
tono_sobre_escala__fine |
222/315 (70 %) | ±0-3 LSB (primeras 16 impresas), misma clase que None (P-10) |
tono_sobre_escala__coarse |
103/157 (66 %) | ±0-4 LSB (primeras 16 impresas), misma clase que None (P-10) |
Matriz completa, 19-sep-2026. Las 16 combinaciones (8 casos × 2 modos) corrieron contra
xsim real bajo Taylor_Series_Corrected. Se cortó una vez a medio camino por un timeout de red
en git push (no un fallo de xsim ni del propio flujo) — el commit local sobrevivió, se
recuperó subiéndolo a mano y se retomó desde el único caso que faltaba
(tono_sobre_escala__coarse).
Comparado caso a caso contra la matriz que ya documentaba None (P-10, y la entrada del
16-sep-2026 en Fases, Z1): el techo de residuo se queda igual
de acotado, ±0-5 LSB en las 16 combinaciones, sin ningún mecanismo nuevo. Los dos casos exactos
(tono_con_offset_dc, ambos modos) siguen siendo exactos con Taylor. El conteo de
discrepancias sube en algunos casos (tono_con_ruido__fine: 56→91;
tono_desbalanceado__fine: 60→87) y baja en otros (tono_sobre_escala__fine: 307→222;
tono_sobre_escala__coarse: 145→103) — no es una tendencia limpia de "mejor" o "peor", y no
debería leerse como una porque el criterio de L1 es bit a bit (D-05): un valor que cae justo en
el borde de redondeo cambia de "coincide" a "discrepa en 1 LSB" por razones de fase, no de
magnitud del error. Lo único que importa para la pregunta que abrió esta migración —¿cambia
Taylor la naturaleza del residuo?— es el techo de LSB, y ese no se movió.
Esto no cierra ZM1 bajo el criterio bit a bit original: 14 de las 16 combinaciones siguen con residuo distinto de cero. Tampoco cierra P-11 en sentido estricto todavía — sigue pendiente el número real de supresión de clutter, igual que ya dejó anotado la decisión D-11. Lo que sí queda resuelto es la pregunta operativa que motivó correr esta matriz: Taylor se comporta en hardware real exactamente como predecían las sondas fuera del árbol, sin sorpresas ni regresiones al migrar.
19-sep-2026: cierre — confirmado que −88/−92 dBc alcanza; criterio de L1 redefinido (D-12)
Confirmación de dominio (vladimir, en sesión): el techo de Taylor (~−88/−92 dBc, medido contra xsim real, ver la tabla de arriba) alcanza para el rango dinámico intra-celda que exige este hallazgo. Con esto se responde la única pregunta que D-11 dejaba abierta ("¿alcanza?") y P-11 cierra.
En la misma sesión se decide aceptar el residuo de cuantización medido (hasta ±5 LSB, 14 de las
16 combinaciones de la matriz de arriba) como el techo normal del bloque NCO en vez de seguirle
exigiendo bit a bit — ver D-12 para el criterio exacto y para lo que falta
(actualizar sim/tb_sv/drx_l1_checker.sv y volver a correr la matriz) para que ZM1 quede
formalmente cumplido bajo el nuevo criterio, no solo decidido.
Lo que sigue abierto, fuera del alcance de P-11: la varianza de los estimadores de momento bajo este residuo, frente a la teoría de Doviak & Zrnic cap. 6 — no evaluada aquí, ver el hallazgo nuevo P-14.
Origen, 8-sep-2026 (sesión): el marco de tolerancia que se venía usando para el residuo de
P-10 (comparar contra el MDS) estaba mal planteado — el error del NCO es multiplicativo (entra
como parte de la mezcla, no como ruido aditivo de piso), así que no mueve la sensibilidad; lo que sí
limita es el rango dinámico intra-celda: la relación clutter-a-señal recuperable en el mismo
gate y el mismo CPI. El canal atenuado (ver hallazgo de arriba, 8-sep-2026, 5 conversores) no ayuda
aquí — cuando el canal de ganancia alta satura por clutter fuerte, la señal débil del mismo gate
solo se puede leer del canal atenuado, cuyo piso está L dB más arriba; el rango explotable es el de
un solo canal, no el compuesto.
Presupuesto derivado (M pulsos de integración coherente, DR_util = SNR_FS_canal + 10·log10(M)):
con espurias sin dither (líneas coherentes que el FFT Doppler integra igual que la señal, no bajan
con M) hace falta P (bits de fase a la LUT) ≥ 18–19; con dither de fase (el error se vuelve ruido
blanco y sí baja con la ganancia del FFT) basta P = 15–16. La amplitud de salida de la LUT (B_a)
tiene una regla aparte: B_a ≥ ancho_datos + 2 para que la cuantización de amplitud quede por
debajo del suelo del datapath — con datos de 16 bits, B_a ≥ 18.
Configuración actual (verificada contra el repo, no contra memoria):
| Parámetro | Valor | Regla del presupuesto |
|---|---|---|
N, nco_phase_bits |
32 | ≥32, cumple |
| P, bits de fase a la LUT | 16 (lut_addr_bits, model/drx_model/nco.py) |
≥18–19 sin dither; 15–16 con dither |
B_a, nco_out_bits |
16 | ≥18 |
| Dither/corrección de fase | ninguno — tcl/gen_ip.tcl no toca Noise_Shaping, queda en el valor de fábrica |
requerido si P=16 |
El peor espurio ya medido contra el modelo dorado (P=16, sin dither, palabra de sintonía 0,24·fs)
es −86,4 dBc (docs/tutorial/03-ddc.md, docs/tutorial/07-resultados.md). Contra el presupuesto
sin dither (−102 a −112 dBc de pico para DR_util ≈ 96 dB con M=64) faltan 15–25 dB.
Verificado contra Vivado real, 8-sep-2026: Output_Width no puede subir sin activar Noise_Shaping
Antes de tocar config/params.toml se probó subir nco_out_bits de 16 a 18 y regenerar
ip/dds_compiler_nco con tcl/gen_ip.tcl (mismo patrón que el resto de este documento: probar
contra la IP real, no conjeturar). Falló en la propia validación de Vivado, no en la
simulación:
ERROR: [IP_Flow 19-3458] Validation failed for parameter 'Output Width(Output_Width)' for
IP 'dds_compiler_nco'. Value '18' is out of the range (3,16)
Aislado por separado (report_property/set_property directos sobre una IP de prueba, fuera
del árbol): con CONFIG.Noise_Shaping {None} (el valor de fábrica, el que usa hoy
tcl/gen_ip.tcl) el rango válido de Output_Width es como mucho 16. Activar
CONFIG.Noise_Shaping a Phase_Dithering o a Taylor_Series_Corrected antes de fijar
Output_Width desbloquea 18 sin error. Es decir: B_a=18 no es alcanzable sin comprometerse
también a una técnica de corrección de fase — no es un cambio de dos campos independientes
como se planteó al proponerlo, los dos están acoplados en la propia IP.
Con esto, el cambio se revirtió íntegro (git checkout -- config/params.toml ip/
rtl/generated/drx_params_pkg.sv tcl/generated/drx_params.tcl fw/include/drx_params.h),
make check vuelve a 74/74 con nco_out_bits=16. No se aplicó ningún cambio de parámetros
con esta sesión — queda registrado aquí en vez de en el árbol.
Las dos técnicas no son intercambiables para este proyecto:
Phase_Ditheringsale de un LFSR interno del propio core — no hay puerto de semilla expuesto (confirmado: la plantilla de instanciación no cambia al activarlo, mismos 4 puertos AXI-Stream). El código VHDL de comportamiento del core está cifrado (`protect, ya confirmado en P-10 para este mismo core) así que el algoritmo del LFSR no es legible. Sin poder reproducirlo enmodel/drx_model/nco.py, la comparación bit a bit de L1 (D-05) deja de tener sentido para el bloque NCO — se rompería exactamente la metodología de verificación de la que depende todo el proyecto, no solo un residuo pequeño.Taylor_Series_Correctedes una técnica determinista y pública (corrección desin(θ+Δ)≈sin(θ)+Δ·cos(θ)con los bits de fase que de otro modo se truncan) — en principio reproducible en el modelo dorado sin necesidad de leer el core cifrado. No probado todavía: falta (a) implementar la corrección enNcocon el mismo orden/redondeo que usa el core, y (b) confirmar contra xsim real que sí es bit a bit reproducible entre corridas antes de declararla compatible con D-05. Es una investigación nueva, del mismo tamaño que la de P-10 (calibración deNCO_LUT_OFFSET_K), no un ajuste de una tarde.
9-sep-2026: primera sonda real de Taylor_Series_Corrected — mejora confirmada, pero pequeña y NO cierra el presupuesto de P-11
Primer intento de responder los dos puntos pendientes de la entrada anterior. Metodología: (a)
reimplementación en Python del algoritmo público de PG141 (tabla gruesa + segunda RAM de
pendiente en la misma dirección) como techo teórico, sin tocar model/drx_model/nco.py ni
ningún fichero comiteado; (b) IP real generada FUERA del árbol (mismo patrón que el resto de
este documento — dds_compiler_nco_taylor_w16/_w18, Noise_Shaping {Taylor_Series_Corrected},
mismos PINC/POFF que hoy, K=655 sin recalibrar) y probada contra xsim real con una sonda
temporal (mismo patrón que la sonda de K en P-10), no comiteada.
Hallazgo metodológico previo, antes de fiarse de ningún número: el "−86,4 dBc" que cita
docs/tutorial/03-ddc.md no tiene detrás ningún script reproducible — no hay test ni tool que
lo calcule, y reproducir su método aparente (FFT con ventana de Hanning, N=8192, igual que
tools/gen_docs_figures.py:figure_nco) da −56,95 dBc, no −86,4: la fuga espectral de la
propia ventana domina sobre el espurio real porque el tono (PINC/2**32 ≈ 0,24, no exacto tras
redondear a 32 bits) no cae en un bin exacto de una FFT de 8192 puntos sin muestreo coherente.
Por eso esta investigación NO usa FFT — mide el error de cuantización directo contra el valor
continuo ideal, en LSB (la misma unidad que ya usa la entrada de K en P-10), sobre
4096 muestras en libre corrida (∼983 vueltas completas de la LUT con este PINC, así que cubre
la LUT entera, no un segmento local).
Confirmado contra xsim real:
- Sí compila y simula con
Noise_Shaping {Taylor_Series_Corrected},Output_Width16 y también 18 (el ancho que el propio Taylor desbloquea, hallazgo de la entrada anterior) — sin errores de validación en ninguno de los dos. - Reproducible bit a bit entre corridas: dos corridas independientes de xsim sobre la misma
sonda de 4096 muestras dan ficheros idénticos (
difflimpio). Esto responde el punto (b) pendiente de la entrada anterior — Taylor SÍ es determinista en la práctica, igual que None. - Coste de recursos, confirmado y ampliado sobre P-12:
dds_compiler_nco_taylor_w16Y_w18sintetizan igual — 3 DSP48 + 1 RAMB18 (0,5 tile) los dos. Subir de 16 a 18 bits de salida con Taylor activado no cuesta nada extra de DSP/BRAM sobre lo que ya midió P-12 para 16 bits. - Hallazgo nuevo, no documentado antes:
Latencysube de 8 (modoNone, config actual) a 9 con Taylor activado.NCO_LUT_OFFSET_K=655se calibró específicamente contraLatency=8(vertcl/gen_ip.tcl) — no hay garantía de que siga siendo el valor correcto con un ciclo más de latencia, y esta sonda NO lo recalibró (lo de abajo usa el mismoK=655sin tocar). - Hallazgo nuevo, sin explicar: la salida real del
sindel core, comparada contrasin_lut[addr] + Δ·cos_lut[addr](la fórmula pública de PG141 tal cual), sale invertida en signo — coincide con-(sin_lut[addr] + Δ·cos_lut[addr]), no con la fórmula directa.cossí coincide en signo. Con esa corrección de signo, la reimplementación en Python llega a un residuo de ±2 LSB (cos) / ±5 LSB (sin) contra la IP real, con solo 4 % de muestras exactas — la misma clase de residuo pequeño-pero-no-cero-ni-explicado que ya tiene el modoNone(ver la entrada de arriba, "el core calcula... con un algoritmo propio... que no coincide LSB a LSB"). No se llegó a una reimplementación bit-exacta — sigue sin poder declararse compatible con D-05 sin más trabajo. -
La mejora medida (mismo
K=655, sin recalibrar, contra el continuo ideal, mismo tramo de 4096 muestras que ya dio 3,08 LSB de pico paraNone):Configuración Pico de error RMS Estimado en dBc (pico / rms) None,Output_Width=16(config actual)3,08 LSB 1,34 LSB −80,6 / −87,8 Taylor,Output_Width=16(mismo ancho, mismoK)2,3 LSB 1,2 LSB −83,0 / −88,8 Taylor,Output_Width=18(el ancho que Taylor desbloquea, mismoK)5,2 LSB (sobre fondo de escala 4× mayor) 3,4 LSB −88,1 / −91,7
Lectura, separando lo confirmado de la inferencia:
- La mejora es real y medida contra el core de verdad, no solo contra un modelo propio — en
los dos anchos, Taylor reduce el error frente a
None. Esto es lo que la pregunta pedía confirmar, y la respuesta es sí. - Pero es pequeña: 5-8 dB, no los 15-25 dB que le faltan al presupuesto de P-11
(−102 a −112 dBc). Con la configuración tal cual (Taylor + 18 bits,
Ksin recalibrar), el suelo medido (~−88 a −92 dBc) sigue estando muy por debajo de lo que el presupuesto de rango dinámico intra-celda necesita. Taylor solo, tal como se probó hoy, no cierra P-11. - Inferencia, no confirmada: la brecha entre el techo teórico de la reimplementación en
Python (con pendiente en punto flotante, error de pico ~0,5 LSB — ver más abajo) y lo medido
contra la IP real (2,3-5,2 LSB) es sospechosamente parecida en tamaño al tipo de error que
corrigió
Ken P-10 (un desfase de ángulo sistemático de ~1 paso de LUT). Es plausible que gran parte de la brecha sea unKmal calibrado paraLatency=9, no un límite intrínseco de Taylor — pero no se comprobó: repetir la búsqueda deK(misma técnica de P-10, sonda de 4096 muestras, barrido de unos pocos valores alrededor de 655) contra esta configuración concreta es el siguiente paso obvio, y es justo el trabajo que la entrada anterior ya estimaba en «del mismo tamaño que P-10» — sigue sin hacerse. - Techo teórico (referencia, no una medida de hardware): la misma reimplementación en Python con la pendiente en punto flotante (sin cuantizar la RAM de pendiente en absoluto) predice un error de pico de solo 0,5 LSB en los dos anchos — el límite de redondeo puro, sin ningún error de fase — equivalente a ~−96 dBc (16 bits) o ~−108 dBc (18 bits). Que lo medido esté tan lejos de ese techo es la evidencia más fuerte de que aquí falta calibración, no que la técnica esté agotada.
Estado: ningún fichero comiteado se tocó — ip/dds_compiler_nco, tcl/gen_ip.tcl,
config/params.toml y model/drx_model/nco.py siguen exactamente como estaban (Noise_Shaping
{None}, K=655, sin cambios). Todo lo de arriba se generó fuera del árbol y no se conserva.
Re-confirmación independiente del coste en DSP48 (mismo día): se sintetizó
drx_ddc_top completo (un canal, synth_design -mode out_of_context, fuera del árbol —
misma copia editada solo para sustituir dds_compiler_nco por
dds_compiler_nco_taylor_w16, el mismo core generado arriba) para no depender solo de la
cifra ya citada en P-12/docs/plataforma/recursos.md. Resultado: 53 DSP48, 1
RAMB18 (0,5 tile) — coincide exactamente con el número de P-12 (50→53, +3 DSP48 por NCO),
ahora verificado dos veces con dos IP generadas por separado, no solo citado. Sobre el
presupuesto de la ZedBoard: el diseño base de 4 canales ya mide 200/220 DSP48 (91 %)
(P-12); activar Taylor en los 4 NCO añadiría 4×3=12 DSP48 → 212/220 (96 %),
dejando solo 8 DSP48 libres (3,6 %) — cabe en el papel, sin margen real de rutado. No
cambia ninguna conclusión de P-12, la refuerza con una segunda medición independiente.
Lo que falta para de verdad cerrar el punto (a) y (b) de la entrada anterior:
- Recalibrar
NCO_LUT_OFFSET_Kespecíficamente paraTaylor_Series_Corrected(Latency=9), misma técnica que P-10 — sin esto, no se sabe cuánto de la brecha frente al techo teórico es real y cuánto es desfase de arranque sin corregir. - Con
Krecalibrado, repetir esta misma medición; si el residuo sigue por encima del presupuesto de P-11 incluso conKcorrecto, ESO sí sería evidencia de que Taylor solo no basta (haría falta corrección cuadrática — PG141 la menciona para SFDR > 120 dB — o aceptar el cambio de criterio de verificación que ya proponía la entrada anterior). - Solo después de (1)-(2): decidir si vale la pena invertir en reproducir el algoritmo bit a
bit en
model/drx_model/nco.py(compatibilidad con D-05) — no tiene sentido antes de saber si la técnica siquiera llega al presupuesto que hace falta.
Lo que falta para cerrar esto:
- El número real de supresión de clutter terrestre requerido y la longitud de CPI (M) objetivo —
no está documentado en ningún sitio de
docs/niconfig/; sin él, cualquier bit exacto dePes una suposición. Mismo patrón que P-01/P-04: silencio de especificación, hace falta preguntar a quien lo sepa. - Con ese número en mano: decidir si el residuo actual (P=16, sin dither, −86,4 dBc) basta o no.
Si no basta, decidir si se invierte en implementar y verificar
Taylor_Series_Correcteden el modelo dorado (compatible con D-05, coste de una investigación completa) o se acepta que el bloque NCO deja de compararse bit a bit y se verifica por presupuesto espectral en su lugar (un cambio de criterio de aceptación de L1, con las implicaciones que eso tiene para ZM1).
9-sep-2026 (segunda sesión): K recalibrado para Latency=9 — confirma que Taylor solo, incluso con K correcto, no cierra el presupuesto
Nota de procedencia: el punto 1 de la lista de arriba (el número real de supresión de clutter y
de M) sigue sin respuesta de especialistas. Por instrucción explícita en sesión (vladimir,
9-sep-2026), esta investigación asume el requisito más exigente en vez de esperar ese
número — el objetivo es que el límite de sensibilidad quede en el frontal analógico, no en esta
cadena digital. Lo de abajo responde el punto (1)/(2) de "lo que falta para de verdad cerrar"
de la entrada anterior (recalibrar NCO_LUT_OFFSET_K para Latency=9 y repetir la medición).
Método: sonda SystemVerilog independiente, fuera del árbol (no comiteada), que corre
dds_compiler en libre corrida con POFF1=0 y vuelca 8192 muestras crudas de TDATA a
fichero. El barrido de K se hace en Python sobre esa única corrida, no regenerando IP por
cada candidato: como POFF1 = (K mod 65536)<<16 solo toca la mitad alta del acumulador de fase
(los 16 bits bajos quedan en 0, ver tcl/gen_ip.tcl), desplazar la dirección de tabla por K en
el propio script de análisis es exactamente equivalente a regenerar la IP con ese POFF1 —
mismo principio que ya usa el resto de este documento, más barato. Barrido grueso (paso 25) más
fino (±30) sobre las 65536 direcciones posibles, para los dos anchos con Noise_Shaping
{Taylor_Series_Corrected} (Latency=9, confirmado igual que en la sesión anterior).
Dos hallazgos de infraestructura nuevos, de la propia sonda, antes de fiarse de ningún número:
create_ip+launch_simulationen el MISMO proyecto Vivado no registra la IP en el fileset de simulación, aunquegenerate_target simulationreporte éxito:get_files -of_objects [get_filesets sim_1]solo devuelve el testbench, nunca el XCI. Hace falta separar, igual que ya separa el árbol real (tcl/gen_ip.tclcrea,tcl/sim_l1.tclsimula en un proyecto aparte víaread_ipsobre el XCI ya en disco) — un patrón que este documento ya conocía pero cuya razón de ser no estaba explicada hasta ahora: no es solo higiene,create_ip+simular en la misma sesión de proyecto sale roto.- El nombre de módulo no puede llegar a la instancia por una macro sustituida solo en
more_options(-d NOMBRE=valor): el escáner de "Fetching design files" delaunch_simulationdecide qué meter en la compilación ANTES de quexvlog/xelabexpandan esa macro, así que nunca ve la instancia y el error ("Module ... not found") aparenta ser un fallo degenerate_targetcuando no lo es. Hace falta el mismo mecanismo que ya usaDRX_HAVE_DDCentcl/sim_l1.tcl:set_property verilog_define {...}sobre el fileset, resuelto por ramas`ifdefcon el nombre en texto literal. - Para
Output_Width=18,M_AXIS_DATA_TDATAreal es de 48 bits, no 36 — cada campo (coseno, seno) se redondea a 24 bits, no a los 18 declarados; confirmado contra el.veoreal (// output wire [47:0] m_axis_data_tdata). ConOutput_Width=16sí coincide (32 bits, ya alineado a byte). Es la misma trampa que ya avisa la cabecera dertl/drx_ddc_top.svpara CIC/FIR ("depende de a qué múltiplo de 8 bits redondea el bus"), confirmada ahora también para el DDS Compiler — relevante el día que se genere un NCO de 18 bits de verdad en el árbol.
Resultado, con los tres hallazgos ya corregidos en la sonda:
| Config | mejor K encontrado | pico | rms | ~dBc pico / rms |
|---|---|---|---|---|
| Taylor, W=16 | 64881 (≡ −655 mod 65536) | 1,89 LSB | 1,15 LSB | −84,8 / −89,1 |
| Taylor, W=18 | 64881 (≡ −655 mod 65536) | 5,00 LSB | 3,31 LSB | −88,4 / −91,9 |
64881 ≡ −655 (mod 65536), y POFF1 resta del acumulador (hallazgo ya documentado en
tcl/gen_ip.tcl) — así que esto es el mismo K=655 ya comiteado, sin cambio, para los dos
anchos. Coincide, dentro del margen esperable de metodología distinta (sonda limpia de libre
corrida vs. la instrumentación de la sesión anterior sobre tb_l1_ddc.sv), con los números ya
reportados el 9-sep-2026 (2,3/1,2 LSB a W=16, 5,2/3,4 LSB a W=18 con el mismo K sin recalibrar).
Conclusión, ahora confirmada por medición directa, no por inferencia: NCO_LUT_OFFSET_K=655
ya era el valor óptimo para Latency=9 — no hacía falta recalibrarlo, y recalibrarlo no
abre ningún margen nuevo. La hipótesis de la entrada anterior ("es plausible que gran parte de
la brecha sea un K mal calibrado... pero no se comprobó") queda descartada. El techo de
Taylor_Series_Corrected en este core, con la configuración que de verdad importa
(Output_Width=18, que no cuesta DSP48/BRAM extra sobre 16 según P-12), es de
~−88 / −92 dBc (pico/rms) — sigue sin llegar a los −102/−112 dBc del presupuesto sin dither
calculado en la entrada anterior.
Medición auxiliar, mismo día — no concluyente, y aquí queda dicho por qué: se generó también
Noise_Shaping {Phase_Dithering} (W=16, mismo método) para tener un número de referencia. El
error crudo por muestra sale peor que Taylor, no mejor: rms=5,72 LSB (~−75,2 dBc) frente a
1,15 LSB de Taylor. Esto no demuestra que dithering sea peor. El argumento de la ganancia de
dither (el mismo que ya usa la sección de presupuesto: "con dither el error se vuelve ruido
blanco y baja con la ganancia del FFT, basta P=15-16") depende de que ese ruido sea de banda
ancha e incoherente con el tono después de integrar M pulsos — una comparación de error
crudo de una sola muestra no puede verlo, mide exactamente lo que el dither está diseñado para
empeorar a propósito (la varianza de una muestra) sin medir lo que está diseñado para mejorar (el
piso espectral tras integración). No se construyó ese modelo de integración hoy — sigue siendo
el paso que de verdad decidiría si dithering ayuda, y con eso pendiente, esta medición auxiliar
ni cierra ni descarta la vía de dithering: solo deja dicho que "menos LSB de error crudo" no es
la pregunta correcta para esa técnica.
Con esto, lo que falta para cerrar P-11 se reduce a dos caminos reales, ninguno gratis:
- Aceptar
Phase_Ditheringy con eso renunciar a la comparación bit a bit del bloque NCO en L1 (su LFSR interno no es reproducible enmodel/drx_model/nco.py, ver la entrada anterior) — sustituirla por un criterio de aceptación espectral/estadístico para ese bloque específicamente. Antes de comprometerse a esto, construir el modelo de integración de M pulsos sobre las muestras ya capturadas (de las tres técnicas) es el trabajo que falta para saber si de verdad compensa. - El número real de M/clutter (punto 1 de la lista de arriba) sigue siendo la única fuente que puede decir si −88/−92 dBc (el techo real de Taylor, ya no una estimación) alcanza o no — la suposición "el número más exigente posible" de esta sesión no sustituye esa respuesta, solo evita que la investigación se quede parada esperándola.
Nada comiteado se tocó: config/params.toml, ip/dds_compiler_nco/, tcl/gen_ip.tcl siguen
exactamente como estaban. Todo lo de arriba vive en scripts fuera del árbol, no conservados.
11-sep-2026: nuevo intento de reproducción bit-exacta — dos hallazgos de infraestructura nuevos, sin cerrar el punto 3 pendiente
Retoma el punto 3 de "lo que falta para de verdad cerrar" (9-sep, primera entrada): reproducir
Taylor_Series_Corrected bit a bit en model/drx_model/nco.py. Metodología, toda fuera del
árbol (sondas de Vivado/xsim generadas y descartadas, ningún fichero comiteado tocado): en vez de
reusar el core Phase_Generator_and_SIN_COS_LUT con el PINC/POFF comiteados, se generaron
IP standalone de dds_compiler para poder controlar la fase de entrada con precisión —
primero variando PINC1 en modo generador de fase, después con PartsPresent
{SIN_COS_LUT_only} (fase de entrada directa por AXI-Stream, sin acumulador interno) para poder
barrer Δ sin depender de ningún PINC.
Hallazgo nuevo #1, confirmado con cuatro valores independientes (PINC1 = 1, 2, 3 y 7):
con un PINC1 pequeño (< 2¹⁶, el acumulador nunca cambia de dirección de LUT dentro de la
ventana de simulación) el core devuelve m_axis_data_tdata con una convención de signo/orden de
campo distinta de la ya documentada y verificada en producción: sin el desfase de arranque de
~655 direcciones, y con los papeles de "coseno" y "seno" intercambiados entre las dos mitades de
la palabra (contrastado sin ambigüedad en una dirección no degenerada, addr=5000, donde
coseno ≠ seno). Con un PINC1 de magnitud grande (se probó tanto el valor real de producción
como uno sintético con los mismos bits altos pero los 16 bits bajos forzados a 1, para poder
barrer Δ de todos modos) el core vuelve a comportarse exactamente como ya documenta este
fichero: hi=coseno (signo correcto), lo=−seno (el signo invertido ya conocido), desfase de
arranque ~655 direcciones. Conclusión: cualquier sonda futura sobre este core tiene que usar un
PINC de magnitud "de producción" — un PINC pequeño para simplificar el barrido, como se
intentó aquí primero, da resultados que no son representativos del core real. No estaba
documentado en ningún sitio de este proyecto; costó tres rondas de xsim real detectarlo.
Hallazgo nuevo #2, en modo SIN_COS_LUT_only (Phase_Width=25, sin acumulador):
generar el mismo core en modo None confirma que Phase_Width no puede superar 16 en ese modo
(ERROR: ... Value '25' is out of the range (3,16)) — es decir, la tabla base sin corregir tiene
como mucho 16 bits de dirección, coincide con lut_addr_bits=16 que ya usa
model/drx_model/nco.py. Con Taylor_Series_Corrected, el mismo core sí acepta hasta 25 bits de
fase de entrada — los 9 bits extra son la resolución de Δ que la corrección de Taylor añade
sobre esos mismos 16 bits de dirección base (no 16 bits de Δ, como asumía sin comprobar la
reimplementación de la entrada del 9-sep).
Corrección sobre la primera versión de esta entrada, mismo día: el primer barrido (tramo
secuencial de 140.000 muestras arrancando en 0) parecía cerrar bien, pero un segundo barrido con
~6100 fases dispersas (aleatorias + puntos estructurados) sobre las 2²⁵ fases posibles dio
errores enormes que esta entrada atribuyó, en un primer momento, a que el core "no se comporta
como una función de latencia fija ante fase no monótona" — ese hallazgo era falso, un error de
alineación en el script de análisis (se restaba la Latency del core al índice de muestra de
salida, pero el testbench ya solo vuelca una línea por ciclo con m_axis_data_tvalid en alto, así
que la salida ya viene alineada 1:1 con el orden del estímulo sin ningún corrimiento adicional).
Corregido el script (sin tocar la sonda ni volver a simular), el mismo barrido disperso de 6106
fases — cubriendo el círculo completo, no un tramo local — cierra igual de bien que el tramo
secuencial. Queda como aviso de método, no como hallazgo del core: al alinear entrada y salida
de una sonda de latencia fija, el índice de la muestra ya capturada (filtrada por tvalid) no se
vuelve a corregir por Latency — restarla dos veces es el error que produjo el falso hallazgo.
Resultado, ya sobre el círculo completo (6106 fases, Output_Width 16 y 18), comparando contra
round(FULL·sin(θ)) / round(FULL·cos(θ)) con la fase de entrada de 25 bits completa (no contra
la fórmula de PG141 sin_lut[addr]+Δ·cos_lut[addr] que probó la sesión del 9-sep):
Output_Width |
canal | máx |error| |
exacto | |error|=1 | |error|=2 |
|---|---|---|---|---|---|
| 16 | "seno" (hi) |
2 LSB | 49,1 % | 47,9 % | 2,9 % |
| 16 | "coseno" (lo) |
2 LSB | 25,1 % | 71,9 % | 3,0 % |
| 18 | "seno" (hi) |
2 LSB | 55,0 % | 41,0 % | 4,0 % |
| 18 | "coseno" (lo) |
2 LSB | 23,7 % | 72,4 % | 3,9 % |
El error nunca supera ±2 LSB en ninguno de los dos anchos, en las 6106 fases muestreadas de las
2²⁵ posibles. Es la misma escala de residuo pequeño-pero-no-cero que P-10 ya documentó y aceptó
para el modo None ("el core calcula sin/cos con un algoritmo propio que no coincide LSB a LSB
con round(FULL·sin/cos(θ)) en coma flotante de doble precisión") — aquí queda confirmado que
Taylor tiene exactamente el mismo carácter de residuo, no uno peor ni de otra naturaleza, y
que el modelo correcto para reproducirlo no es reconstruir la aritmética interna de tabla+pendiente
de PG141 con su ancho de bits propio (lo que intentó la sesión del 9-sep, con más residuo y sin
llegar a esta conclusión), sino evaluar la función continua a la resolución total de 25 bits que
el Δ de 9 bits permite — mucho más simple, y con el mismo techo de error ya aceptado en el resto
del proyecto.
Conclusión de esta sesión: el punto 3 pendiente (reproducción bit-exacta de Taylor, compatible
con D-05) sigue sin cerrarse en sentido estricto — el residuo de hasta ±2 LSB no es cero en
~50-75 % de las muestras — pero ya no es un misterio de qué tan grande es ni dónde: está acotado,
medido en las 2²⁵ fases posibles (no solo un tramo local, corrección de la versión anterior de
esta entrada) y es idéntico en carácter y magnitud al residuo que el proyecto ya acepta para
el modo None desde P-10. Eso cambia la lectura de la disyuntiva que P-11 dejaba abierta: si el
criterio de aceptación de L1 ya tolera este residuo para None, extenderlo a Taylor no es
aceptar una regresión — es aceptar la misma clase de aproximación que ya rige hoy para el resto
del bloque NCO. No cambia la conclusión de rango dinámico ya cerrada: el techo de Taylor
(~−88/−92 dBc) sigue siendo el mismo, esta sesión no volvió a medirlo. El hallazgo #1 (PINC
pequeño da una convención de campo/signo no representativa) sigue siendo válido y es el resultado
de infraestructura más reutilizable de hoy para quien vuelva a sondear este core.
Nada comiteado se tocó: todas las IP, testbenches y scripts de esta sesión viven en
/tmp (scratch de la sesión), no en el árbol.
16-sep-2026: sonda exploratoria de integración de M pulsos — debilita la hipótesis pesimista del presupuesto, pero no la sustituye
Retoma el punto que dejaba abierto la "medición auxiliar" del 9-sep (segunda sesión): el
presupuesto de este documento asume que el espurio de cuantización sin dither es "una línea
coherente que el FFT Doppler integra igual que la señal" (mejora 0 dB con M) y que el de
Phase_Dithering sí baja con la ganancia del FFT — pero nadie había construido ese modelo de
integración, solo enunciado el argumento. Esta entrada lo construye, en Python, fuera del árbol,
reusando Nco/NcoTaylor de model/drx_model/nco.py (sin tocarlos).
Método: un "gate" = un offset de fase inicial fijo (una posición de rango). El NCO corre en
continuo entre pulsos, así que la fase en el pulso m del mismo gate es la del acumulador en
n = m·PRI_samples, con PRI_samples = prf_div (valor de registro, idéntico en las dos
plataformas — se usaron los dos extremos ya declarados en config/params.toml,
prf_div_min/prf_div_max, más el caso de 326 Hz que cita D-09). Error de
cuantización por pulso = salida de Nco/NcoTaylor menos la referencia continua ideal
(FULL·cos/sin del ángulo de 32 bits sin truncar) — la misma definición de espurio que ya usan
los números de arriba. Phase_Dithering se modeló como ruido i.i.d. gaussiano con el rms YA
MEDIDO el 9-sep contra IP real (5,72 LSB, Output_Width=16) — un proxy de MEJOR CASO
(incoherencia total pulso a pulso), no una medición: el LFSR real es opaco (VHDL cifrado) y no se
sabe si decorrelaciona así a la cadencia real de PRI. Se barrieron 8 gates aleatorios y
M ∈ {16, 64, 256}; se midió el pico del espectro lento tanto en el bin DC como excluyéndolo
(el bin DC es donde cae el clutter fijo, que el propio presupuesto ya da por perdido/rechazado —
el bin que de verdad importa es cualquier otro, donde podría cazerse un blanco real).
Resultado, peor caso sobre los 8 gates:
| PRI | M |
técnica | dBc/1 pulso | dBc/M, excluyendo DC |
mejora |
|---|---|---|---|---|---|
| 1200 Hz | 256 | None |
−79,3 | −95,2 | −15,9 dB |
| 1200 Hz | 256 | Taylor |
−96,2 | −116,2 | −19,9 dB |
| 200 Hz | 256 | None |
−79,5 | −97,8 | −18,3 dB |
| 200 Hz | 256 | Taylor |
−96,2 | −107,2 | −11,0 dB |
| cualquiera | 256 | Dither (sintético i.i.d.) | ~−64 | ~−90 | ~−26 dB |
(tabla completa, las tres PRI × tres M × tres técnicas, en el script — no conservado, ver
"Estado" más abajo).
Lectura, separando lo que esto sí muestra de lo que NO puede mostrar:
- Sí debilita la hipótesis pesimista: ni
NoneniTaylorse comportan como una línea perfectamente coherente que no mejora nada conM— en los tres PRI probados, ambos ganan entre 5,5 y 20 dB al integrar 256 pulsos. La suposición binaria del presupuesto ("sin dither no baja conM") es más pesimista que lo que este modelo simplificado predice. - Pero el efecto es errático, no una ley limpia: la mejora depende fuertemente de qué PRI se
elige (5,5 a 20 dB, sin patrón monótono claro con
M), y el pico del espectro lento no siempre cae lejos de DC — para PRI=200 Hz, el pico fuera de DC es en realidad peor que el de DC. No es un resultado que se pueda generalizar a "la PRF que sea" sin más trabajo. - Dos huecos que esto NO modela, y que importan más que el resultado numérico:
- El modelo de
NcoTaylorusado aquí llega al piso teórico de cuantización de fase (~0,5 LSB, ~−96 dBc) — NO al techo real de hardware ya medido el 9-sep conKrecalibrado (1,89–5,00 LSB, −84,8/−88,4 dBc), que es el error de implementación interna del core, no reproducible sin leer el VHDL cifrado. No se sabe si ESE residuo (el que de verdad limita hoy) decorrelaciona igual entre pulsos — esta sonda no puede verlo. - El origen de P-11 (8-sep) es explícito: el error del NCO es multiplicativo, entra mezclado con el clutter, no como piso aditivo. Esta sonda mide el espectro lento del error del NCO aislado — no modela la modulación cruzada con un retorno de clutter fuerte, que es el mecanismo real que preocupa (podría producir una banda lateral del propio clutter en un bin de Doppler ajeno a DC, justo donde se buscaría un blanco débil). Construir ESE modelo es un trabajo distinto y más grande.
- El modelo de
Conclusión: esto no cierra ni descarta la vía de M pulsos — quita a la hipótesis pesimista
del presupuesto la certeza con la que se citaba, sin poder afirmar por eso que el problema esté
resuelto. Sigue sin sustituir al bloqueante real: el número de especialistas de dominio (punto 1,
más arriba) sigue siendo la única fuente que puede decir qué margen hace falta de verdad, y sigue
siendo también quien puede decir si "peor bin fuera de DC" es siquiera el criterio correcto para
esta cadena.
Estado: ningún fichero comiteado se tocó — el script vive en /tmp (scratch de la sesión), no
se conserva. Nco/NcoTaylor no se modificaron para esta sonda.
P-12 · Presupuesto de DSP48 en ZedBoard: el diseño de 4 canales ya no cabe con holgura¶
Estado: cerrado para la ZedBoard, 9-sep-2026 (ver la entrada de cierre más abajo) — la
arquitectura de 4-5 canales reales se difiere a la ZU9. Dueño: equipo (RTL) · Bloqueaba: la
decisión de resource-sharing entre canales, que rtl/drx_ddc_quad_top.sv daba por innecesaria para
ZM1; y la viabilidad de la vía Taylor_Series_Corrected que evalúa P-11.
Origen, 9-sep-2026: al evaluar si la ZedBoard tiene multiplicadores de sobra para
Taylor_Series_Corrected (P-11) considerando los 5 conversores del frontal (A10), se sintetizó de
verdad drx_ddc_top/drx_ddc_quad_top (synth_design -mode out_of_context, XC7Z020-1 grado -1,
tal como están hoy en el árbol) en vez de seguir citando la tabla estimada de
docs/plataforma/recursos.md. El resultado desmiente esa tabla, no solo responde la pregunta de
P-11:
| Configuración | DSP48 | Sobre 220 disponibles |
|---|---|---|
1 canal DDC completo, Noise_Shaping {None} (config actual) — medido |
50 | 23 % |
1 canal, NCO con Noise_Shaping {Taylor_Series_Corrected} — medido |
53 | 24 % |
| 4 canales Rx, config actual — medido, no extrapolado | 200 | 91 % |
| 4 canales + Taylor en los 4 NCO — extrapolado del delta medido (+3/canal) | 212 | 96 % |
| 5 canales (si el conversor de Tx necesita cadena DDC propia, A10 sin decidir) | 250 | 114 % — no cabe |
| 5 canales + Taylor | 265 | 120 % — no cabe |
docs/plataforma/recursos.md estimaba ~34 DSP48 (15 %) asumiendo FIR time-shared entre 8
streams (10 DSP48 total). Esa no es la arquitectura real: la cabecera de
rtl/drx_ddc_quad_top.sv es explícita — sin resource-sharing entre canales, cada uno instancia
su propio juego completo de IP, y ese diseño es el que ya está commiteado, no una alternativa
hipotética. La tabla se corrigió con los números medidos (docs/plataforma/recursos.md).
Lo que esto cambia, en orden de urgencia:
- El DSP48, no el cierre de temporización, es hoy el recurso más ajustado del proyecto en ZedBoard. Con 4 canales al 91 % sin haber tocado Z2 (motor de timing), Z3 (SSI) ni el DMA, cualquiera de esos bloques que necesite un multiplicador (contadores de jitter con escalado, validación de PRF×rango, lo que sea) compite por los 20 DSP48 que quedan libres.
- P-11 (Taylor_Series_Corrected) pasa de "¿es determinista?" a "¿cabe?". Con los 4 canales actuales sí cabe en el papel (212/220) pero sin margen real de rutado — 96 % de un recurso concentrado geográficamente en el die es un problema de congestión, no solo de aritmética.
- Si el conversor de Tx (A10) necesita su propia cadena DDC, la arquitectura de hoy no cabe en la ZedBoard en ningún caso — 250 DSP48 sobre 220 disponibles, con o sin Taylor. Esto es independiente de P-11 y hay que cerrarlo primero: no tiene sentido decidir el presupuesto de bits del NCO de un canal que igual no puede replicarse una quinta vez.
Condición de cierre original: una de las dos — (a) implementar resource-sharing real entre canales para los bloques de mayor consumo (FIR compensador/semibanda, que es lo que la tabla original ya asumía compartido) y volver a medir, o (b) una decisión explícita de que 4 canales sin compartir es suficiente para ZM1 y que Z2-Z4 se presupuestan contra los ~20 DSP48 que quedan — con el conversor de Tx (A10) resuelto primero, porque decide si el número de partida es 4 o 5. Ver la entrada de cierre de abajo: se tomó una tercera vía, ni (a) ni (b).
9-sep-2026: decisión — cadena de Tx independiente, sin compartir; en la ZedBoard, dos configuraciones de 3 canales en vez de 4-5; la combinación completa se prueba en la ZU9
Cierra, para el alcance de la ZedBoard, la pregunta de A10 sobre la muestra de Tx y la condición de cierre de esta entrada. Se evaluó explícitamente una tercera vía antes de decidir, distinta de las (a)/(b) de arriba: compartir en el tiempo la cadena DDC de un canal Rx entre la muestra de Tx y el rayo que le sigue, aprovechando que la muestra de Tx llega antes del rayo con un hueco entre ambos.
Por qué se descartó compartir — el transitorio de la propia cadena, no una intuición: el
CIC (N=5, R=52) más el FIR compensador (63 taps, y la semibanda de 23 taps en modo grueso) tardan
en "olvidar" por completo la señal anterior — es el mismo número que ya usa
model/drx_model/l1_vectors.py:_settling_in_samples para marcar el arranque de L1, aplicado aquí
a un segundo transitorio a mitad de la ráfaga, no solo al de encendido:
| Modo | Transitorio de la cadena compartida | Zona ciega en rango equivalente (c·t/2) |
|---|---|---|
| fino (R1=208) | 35,9 µs | ~5,4 km |
| grueso (R2=416) | 83,7 µs | ~12,6 km |
Ese hueco de "no fiarse de la cadena" cae exactamente al principio del rayo, es decir, en el
rango más cercano — la parte de la cobertura que casi ningún radar puede permitirse perder. El
hueco real Tx→rayo de este diseño no está documentado en ningún sitio del repo (no hay nada
bajo "T/R", "blanking", "conmutación" ni "guard" en docs/), pero un conmutador T/R típico
recupera en el orden de cientos de ns a pocos µs — muy por debajo de los 36-84 µs que la cadena
necesita. Compartir habría cambiado sin avisar un ahorro de ~50 DSP48 por una zona ciega de rango
cercano de varios km, más lógica de descarte de gates y verificación L1 nueva (el transitorio de
mitad de ráfaga no arranca de registros en cero, así que el argumento bit-a-bit de hoy para la
ventana de arranque no se traslada sin más). No compensaba.
Decisión: la muestra de Tx tiene su propia cadena de procesamiento independiente, sin compartir con ningún canal Rx.
Decisión, alcance ZedBoard únicamente (plataforma experimental — ver la advertencia de
CLAUDE.md, esto no es la arquitectura final de la ZU9): en vez de forzar los 4-5 canales reales
del frontal analógico de una vez (que ya dejaba el DSP48 al límite, ver la tabla de arriba), se
prueban dos configuraciones de 3 canales, por separado:
- Config A — cadena de Tx + H + V (las dos polarizaciones, ganancia nominal): valida la cadena de Tx y la diversidad de polarización.
- Config B — cadena de Tx + canal nominal + canal atenuado, una sola polarización: aísla el estudio de rango dinámico intracelda de P-11 sin necesitar las dos polarizaciones a la vez.
Presupuesto de DSP48 para cualquiera de las dos, peor caso (si la cadena de Tx cuesta igual que un canal Rx completo — no confirmado, ver "lo que falta" abajo):
| Config (3 canales) | DSP48 | Sobre 220 |
|---|---|---|
| sin Taylor | 150 | 68 % |
| con Taylor en los 3 NCO | 159 | 72 % |
Margen real de 28-32 %, frente al 4-9 % que dejaba la config de 4-5 canales — resuelve la urgencia #1 de esta entrada sin necesitar resource-sharing.
La combinación completa (4 o 5 canales reales a la vez) se prueba en la ZU9, donde hay mucho más DSP48/BRAM disponible — no es un riesgo aceptado en silencio, es un aplazamiento explícito a la plataforma que sí lo soporta con margen. La ZedBoard sigue sin validar JESD204B, 250 MSPS ni el frontal analógico real (la misma advertencia de siempre); esto solo decide con cuántos canales se ejercita la lógica digital mientras tanto.
Lo que falta para ejecutar esto — no es solo config:
rtl/drx_ddc_quad_top.svhoy es 4 canales Rx homogéneos, sin resource-sharing — ninguna de las dos configs es un subconjunto directo de ese módulo. Hace falta un top-level nuevo (o el mismo con generics) para 3 canales heterogéneos. Hecho, 9-sep-2026: el mismodrx_ddc_quad_top.sv, instanciado conN_CH=3— no hacía falta un módulo nuevo, es genérico enN_CHy elgenerateno distingue Rx de Tx. Regresión L1 nueva para esta instanciación:sim/tb_sv/tb_l1_ddc_tri.sv+tcl/sim_l1_tri.tcl, con gate de lint enmake check(lint-hdl-tb-tri). Corrida real contra xsim (-platform zedboard -mode fine, tres casos distintos por canal para descartar cruce entre ramas delgenerate): elabora y corre limpio, sinERROR/CRITICAL WARNING— el canal 0 (tono_if) da 303/315 discrepancias de ±2-3 LSB en régimen permanente, la MISMA clase de residuo pequeño y ya aceptado que documenta P-10 para este caso concreto (no es un hallazgo nuevo). El testbench aborta en el primer canal con discrepancias — mismo diseño que ya teníatb_l1_ddc_quad.sv— así que los canales 1 y 2 (tono_desplazado,dos_tonos) no llegaron a compararse en esta corrida; repetirla varía el orden de casos por canal si se quiere cubrirlos primero.-mode coarsecorrido, mismo día: también limpio, sinERROR/CRITICAL WARNING; canal 0 (tono_if__coarse) da 3/157 discrepancias (2 %), que coincide exacto con el número ya documentado más arriba en la matriz de P-10 conK=655— confirma que el top-level de 3 canales reproduce el comportamiento ya aceptado, no un caso nuevo. Cada corrida real tarda con este simulador:fine71 min de reloj,coarse69 min.- Confirmar si la fuente de muestra de Tx tiene el mismo ancho/framing que un canal Rx o
necesita su propia variante de
drx_ddc_top— no verificado en esta sesión. Resuelto, 9-sep-2026 (decisión de equipo, vladimir): mismo ancho/framing que un canal Rx —drx_ddc_topno distingue Tx de Rx, así que no hace falta variante propia. - Actualizar el hito ZM1 en
docs/implementacion/fases.mdpara reflejar las dos configs en vez de "los cuatro canales" (hecho, ver esa página). - Ampliación con
drx_range_gate(Z1, 10/11-sep-2026):sim/tb_sv/tb_l1_ddc_range_tri.sv+tcl/sim_l1_range_tri.tclencadenandrx_ddc_quad_top(N_CH=3) condrx_range_gate_quad_topsobre los mismos tres casos del punto 1, y comparan tambiénm_last(TLAST) por rayo, no solo la muestra decimada suelta — cierra la limitación citada arriba ("repetirla varía el orden de casos por canal si se quiere cubrirlos primero"). Corrida real-mode fine: TLAST coincide bit a bit en los tres canales (288/288/288 muestras) — confirma que el framing del rayo cae donde toca, alimentado por la IP real de Xilinx, no un generador sintético. El dato detono_ifen canal 0 (276/288 discrepancias, ~96 %) reproduce la misma proporción que ya documenta el punto 1 (303/315, ~96 %): confirma quedrx_range_gateno toca el dato, solo lo reordena en el tiempo. Se probó también, antes de fijar el mapeo anterior, cambiartono_ifportono_desplazadoen el canal 0 para poder comparar dato hasta el final en un caso distinto: salió la MISMA clase de residuo (±1-3 LSB) pero mucho menos frecuente — 44/288 (~15 %) — el primer dato real sobre un caso que nunca se había comparado hasta el final (el aborto del checker en el primer canal con discrepancias tapaba esto también). Esto sugiere que la severidad del residuo depende de la señal/frecuencia de cada caso, no es uniforme entre casos — no se investigó la causa, y no cambia que el residuo ya está aceptado como clase de error paratono_ifen P-10. Hallazgo de infraestructura de prueba, no de diseño: el checker compartido aborta con$fatalen el primer canal con discrepancias de dato, y eso tapaba el resultado de TLAST aunque ya estuviera completo (el contador de TLAST corre en unalways_ffcontinuo desde el reset, independiente del checker de dato). Se corrigió el ORDEN de las comprobaciones dentro detb_l1_ddc_range_tri.sv(TLAST se reporta antes que el dato) para que este tipo de residuo aceptado deje de esconder para siempre si TLAST está bien o mal. No se tocótb_l1_ddc_tri.sv, que sigue con la limitación original (documentada en el punto 1). Vectores dorados idénticos a los del punto 1, ya generados portools/gen_l1_vectors.py, incluido un cuarto caso (tono_desplazado_neg) usado solo en el experimento de arriba y descartado del mapeo final. Cada corrida real de este testbench tarda ~75-80 min de reloj en esta máquina.-mode coarsecorrido, 11-sep-2026: también limpio, sinERROR/CRITICAL WARNING. TLAST coincide bit a bit en los tres canales (144/144/144 muestras) — la misma confirmación que enfine, ahora también en coarse. El checker aborta, como siempre, en el primer canal con discrepancias de dato: canal 0 (tono_if__coarse) da 3/144, que coincide exacto con el 3/157 ya documentado en el punto 1 para este mismo caso sin range gating — confirma otra vez, ahora en coarse, quedrx_range_gateno toca el dato. Con esto quedan cerradas las dos combinaciones (fine/coarse) de esta ampliación;tono_desplazadoydos_tonosen coarse siguen sin comparar su dato hasta el final por el mismo aborto del checker en el primer canal — no es una limitación nueva de esta corrida.
19-sep-2026: primer P&R real de drx_ddc_quad_top + drx_range_gate_quad_top dentro de drx_top — confirma la cifra fuera de contexto, y encuentra dos hallazgos de síntesis que xsim nunca vio
Hasta hoy, todo lo de arriba (incluida la tabla de DSP48 de esta entrada) salió de síntesis
fuera de contexto (synth_design -mode out_of_context apuntando directo a
drx_ddc_top/drx_ddc_quad_top) o de simulación funcional (xsim). Con vector_source escrito
y rtl/drx_top.sv instanciando de verdad la cadena completa (ver
Fases, Z1 para el detalle), se corrió por primera vez el flujo
real de tcl/build.tcl hasta -step impl (P&R completo, no fuera de contexto).
DSP48 real, post-route: 153/220 (69,6 %) — coincide exacto con la fila "1 canal... — medido"
de la tabla de arriba multiplicada por 3 canales, confirma que la extrapolación fuera de
contexto no mentía. LUT 7,6 %, BRAM Tile ~21 % (a 62,5 MHz, después de bajar el reloj — ver
abajo). Es la cifra real de Config B (Tx + nominal + atenuado) tal como está hoy — Config A no
se ha medido por separado, N_CH=3 no distingue cuál de las dos.
Dos hallazgos que el flujo out-of-context/xsim nunca hubiera mostrado, los dos en
drx_range_gate.sv (RTL ya verificado bit a bit contra xsim, no código nuevo): sus arrays
mem0/mem1 no sintetizaban a Block RAM (primero por vivir en un proceso con reset asíncrono
en su sensibilidad, después porque el patrón de lectura con dos condiciones anidadas seguía sin
reconocerse como BRAM aunque se forzara ram_style="block" — Vivado lo marcaba "Infeasible").
Corregido separando cada banco en procesos de lectura/escritura propios, sin ninguna sentencia
compartida entre mem0 y mem1; reverificado contra xsim real (fine y coarse) antes de seguir,
mismo resultado bit a bit / D-12 que antes de tocar nada. Detalle completo en
Fases, Z1.
Con el datapath real ya sintetizando de verdad, el primer P&R completo a 100 MHz no cerró
(WNS = −1,209 ns) — la contingencia de docs/plataforma/dilatacion-temporal.md (bajar a 62,5
MHz) resultó necesaria, no solo teórica. Aplicada, misma sesión: fs_hz de zedboard baja a
62,5 MHz en config/params.toml, WNS pasa a +2,770 ns. No cambia nada de lo que esta entrada
documenta sobre presupuesto de DSP48/BRAM — la dilatación temporal (D-02) no toca área, solo
velocidad de reloj.
Supuestos de entorno a verificar¶
Estos son supuestos del plan de ZedBoard. Todo número marcado como estimación en este sitio es una inferencia a partir de hojas de datos y debe verificarse en banco antes de comprometerse a él.
| # | Supuesto | Cómo verificarlo |
|---|---|---|
| A1 | ZedBoard rev. D o posterior, XC7Z020-CLG484-1, 512 MB DDR3 | Serigrafía de la placa |
| A2 | Vivado/Vitis con licencia para DDS/CIC/FIR Compiler y AXI DMA | report_ip_status sobre un proyecto de prueba |
| A3 | El proyecto DSP aún no ha congelado el contrato DRx↔DSP | Preguntar al equipo DSP |
| A4 | El RTL custom se puede escribir en SystemVerilog, sin mandato VHDL | Decisión de equipo — ver D-05 |
| A5 | La placa/MCU auxiliar para SSI tiene al menos un periférico SPI esclavo o PIO a ≥2 MHz | Hoja de datos del MCU |
| A6 | Hay acceso a un osciloscopio de ≥100 MHz para medir jitter de trigger | Inventario de laboratorio |
| A7 | Existe un runner de CI con Vivado y licencia para el job gateware |
Cerrado el 21-sep-2026: la máquina de desarrollo se registró como runner self-hosted (lamula-drx-dev-vivado, label vivado), instalado como servicio systemd, HAS_VIVADO_RUNNER=true. Confirmado corriendo el job gateware de verdad, no a mano: substrato y síntesis+bitstream de zedboard cierran verdes (WNS = 2,489 ns). El bloqueo de gh auth login era de credenciales (necesitaba --web/PAT), no arquitectónico — el token de registro se generó desde la web de GitHub, sin depender de gh autenticado en esta máquina. La red tenía además un Pi-hole bloqueando por DNS varios subdominios de actions.githubusercontent.com necesarios para que el runner reciba jobs; blanqueados. Detalle completo en fases.md#z1, entrada del 21-sep (más tarde) |
| A8 | La versión de Vivado está pinada en el runner | El runner (única máquina, la de desarrollo) corre Vivado ML Standard 2022.2 de facto, pero nada en CI lo verifica ni lo pinea explícitamente — sigue siendo trabajo aparte si se quiere un gate duro |
| A11 | El Vivado 2022.2 del runner tiene el device pack de Zynq UltraScale+ instalado (para el part xczu9eg-ffvb1156-2-e de la ZU9) |
No lo tiene (hallazgo del 21-sep-2026): get_parts xczu9eg* y get_parts -filter {ARCHITECTURE == zynquplus} devuelven cero. El leg zu9 de la matriz de CI nunca se había corrido antes — ni en CI ni a mano — así que este hueco no se había detectado. Se sacó zu9 de .github/workflows/ci.yml hasta que se instale el device pack (descarga grande vía instalador de Xilinx); config/params.toml no tiene ningún error, el part configurado es correcto |
| A9 | El sistema opera con una referencia de reloj externa disciplinada por GPS, 10 MHz + 1PPS, local a un único radar — no compartida entre sitios de la red LAMULA (confirmado en sesión, 7-sep-2026). Es, con altísima probabilidad, la "referencia de 10 MHz de la casa" que asume ADR-001 | Falta la ficha técnica del GPSDO (formato de señal de las dos salidas, conector, redundancia) y confirmar si la portadora ZU9/FMC213 candidata tiene entrada dedicada para 10 MHz y para 1PPS. Relevante para P-03 (árbol de clocking/SYSREF) y para el diseño de metadatos de Z4 (¿el 1PPS entra al marcado temporal del rayo, o solo disciplina el reloj de muestreo?) |
| A10 | El frontal analógico tiene 5 conversores: 2 por polarización (H, V) más 1 para la muestra del transmisor (confirmado en sesión, 8-sep-2026 — ver hallazgo arriba). n_rx_ch=4/n_tx_burst_ch=1 y el contrato v0.4 ya reflejan esta cuenta |
Falta confirmar con quien conoce el frontal qué distingue a los dos conversores de una misma polarización entre sí, para poder nombrar rx_0..rx_3 con algo más específico que "Canal Rx IF N" en el contrato y en docs/plataforma/fuente-de-muestras.md; y avisar al proyecto DSP del bump de contrato (D-08) |
Presupuesto de 1GbE: confirmado, con una salvedad¶
Con R = 208, 4 canales Rx e I/Q de 16+16 bits:
tasa por canal = 250e6 / 208 = 1,2019 MSPS complejas
bytes por canal = 1,2019e6 × 4 B = 4,81 MB/s
4 canales = 19,23 MB/s = 153,8 Mbps
+ overhead Ethernet (MTU 1500, ~2,8 %) ≈ 158 Mbps
Encaja con holgura 6× en 1GbE y coincide con el «~20 MB/s» del plan original, que puede afirmarse con confianza. La salvedad: ese número asume ciclo de trabajo del ~100 % en el gating, que es exactamente el peor caso cuando la PRF se lleva al límite del rango (D-09). Así que 19,23 MB/s es el techo, no un promedio.
Riesgos propios de la estrategia ZedBoard¶
| Riesgo | L | I | Mitigación |
|---|---|---|---|
| El riesgo #1 (JESD) queda sin retirar por tiempo indefinido | Alta | Alta | Decisión de compra (P-03). No hay mitigación técnica |
Deuda de portabilidad por relajar la disciplina de FS_HZ/SSA |
Media | Alta | Revisión de portabilidad obligatoria en cada PR; registro delta vivo; el CI falla si aparece una constante de reloj hardcodeada |
| El contrato DRx↔DSP no se congela porque el proyecto DSP va a otro ritmo | Media | Alta | Congelar v0.1 unilateralmente y versionar; negociar sobre algo concreto |
| El 7Z020-1 no cierra a 100 MHz con 4 canales | Media | Baja | Bajar a 62,5 MHz o a 2 canales; numéricamente idéntico |
| Ancho de banda de DDR insuficiente para reproducir 4 canales | Media | Baja | Los modos BRAM-loop y Throttled ya lo cubren |
| El equipo confunde «validado en ZedBoard» con «validado» | Media | Alta | El registro delta se lee en cada demo de sprint; ningún hito se declara sin él |
Integración extremo a extremo con DSP y RCP reales¶
P-13 · Ningún plan conecta la ZedBoard real con DSP y RCP reales¶
Estado: abierto, fase C0 cerrada el 28-sep-2026 · identificado 2026-09-17 · Dueño: los
tres equipos (DRx + DSP + RCP), no tiene dueño único hoy · Bloquea: poder ejercitar con
vector_source (datos generados en el PL, sin ADC ni JESD204B — ver P-03, que este
pendiente no sustituye ni mitiga) todo lo que la ZedBoard sí permite probar, contra los otros dos
proyectos reales en vez de contra un stub.
28-sep-2026: C0 — el DSP real y el RCP real ya hablan entre sí
El plan de fases de más abajo gana una fase C0 antes de la B, porque la mitad de la
integración no necesita esta placa y estaba bloqueando a la otra mitad sin motivo: los binarios
reales del DSP y del RCP nunca se habían conectado entre sí. Ahora sí, con un emisor sintético
en lugar de la ZedBoard (drx_faker, un ejemplo del repo del DSP que habla el contrato
DRx↔DSP por TCP de verdad). Banco: tools/hil/run-c0.sh del repo lamula-rcp, documentado en
su página de banco C0.
A este proyecto no le tocó nada — C0 no toca el contrato drx_dsp, ni el RTL, ni el
firmware. Pero sí importa lo que encontró, porque son fallos que estaban esperando a la Fase B
y que habrían aparecido con la placa delante, que es el peor momento para depurarlos:
- Los dos extremos del enlace
DSP↔RCPescuchaban. Nadie conectaba: ese enlace nunca se había levantado entre los binarios reales. Se fijó la regla "el productor conecta" para los dos enlaces de la cadena, así que el firmware de Z4.2 de este proyecto tiene que conectar al DSP, no escuchar. El DSP ya escucha enLAMULA_DSP_DRX_ADDRy no cambia. - Todos los radiales salían con ancho de azimut cero. El DSP se quedaba con el azimut del
primer pulso del radial y publicaba
az_end_deg = az_start_deg. Corregido allí. Lo que le toca a este proyecto: cuando Z3 ponga az/el reales en cada rayo, el azimut tiene que avanzar dentro del radial — es lo que el consumidor usa para reconstruir el sector, y un radial de ancho cero no es archivable como Level-II ni aceptable para ORPG.
Lo que C0 no prueba, y sigue igual: nada de hardware. Ni cadencia, ni jitter, ni contrapresión reales. Eso es la Fase B, y sigue esperando al firmware de Z4.
Revisión de integración cruzada (2026-09-17) encontró que cada proyecto para su cadena de pruebas en su propia frontera, y ninguno de los tres planes la cruza:
- Z4 de este proyecto (
docs/implementacion/fases.md#z4) fija el hito ZM4 como «rayos completos consumidos por un stub de DSP sobre 1GbE» — no el binario real del proyecto DSP. - El plan del DSP (
dsp-plan.md§8.2, Fase 1) valida su M1 contra su propio simulador sintético de I/Q, no contra tráfico real de un DRx corriendo en hardware. - El RCP (
lamula-rcp/docs/alcance/pendientes.md, PEND-RCP-05 y PEND-RCP-13) solo ha ejercidoadapters/dsp/moment_stream_receiver.pycontra un stub propio y contraradar_emulator— ninguno de los dos tiene relación con este proyecto. - Los tres planes maestros (
rcp-plan.md,dsp-plan.mdde LAMULA DSP) fijan el criterio de éxito de mes 8 como «validado extremo a extremo contra simuladores propios»; el hardware real queda explícitamente para «field commissioning», después del mes 8, sin fecha ni plan operativo — solo la promesa de un documento de plan (FPGA-commissioning dry-run plan, M4 de ambos planes).
Lo que sí se puede probar hoy, sin esperar a la ZU9 ni al frontal analógico real: todo lo que
está aguas abajo de la SSA (docs/plataforma/fuente-de-muestras.md) es indiferente a si la fuente
es vector_source o jesd204b_source. Eso incluye, con vectores conocidos inyectados desde el
PL: la cadena DDC completa, el range gating en los dos tamaños de celda, el motor de timing/PRF/
triggers, el etiquetado az/el por los lectores SSI, el ensamblado de rayos en FreeRTOS, el AXI DMA,
el framing DRx↔DSP real sobre 1GbE físico (no un stub), la ingesta y los algoritmos del DSP real
alimentados por la cadencia y el jitter de hardware de verdad (no sintéticos), el tráfico real
DSP↔RCP, y el control/archivo/MMI del RCP real. Lo que no se puede probar así, y sigue siendo
P-03: JESD204B, clocking/SYSREF, 250 MSPS, el frontal analógico y el DAC AD9129.
Condición de cierre: un hito conjunto, acordado por los tres equipos, que sustituya el «stub de
DSP» del ZM4 de este proyecto por el binario real del DSP, y que ese DSP real alimente al RCP real
(no a su stub ni a radar_emulator) — banco de pruebas de hardware-in-the-loop con datos 100%
generados en el PL. Hoy no existe ni la fecha ni el documento que decida en qué semana de cuál de
los tres calendarios (28 de este proyecto, 34 de DSP, 34 de RCP) entra ese hito. Ver también la nota
correspondiente en lamula-dsp/docs/algorithms/roadmap.md (sección "Abierto (cross-proyecto):
banco de pruebas extremo a extremo contra ZedBoard real") y PEND-RCP-13/PEND-RCP-14 en
lamula-rcp/docs/alcance/pendientes.md.
Plan por fases (2026-09-17), para dar contenido concreto a la condición de cierre de arriba. Ninguna fase depende del frontal analógico; todas caen dentro del límite de P-03. Ninguna está agendada en ningún calendario todavía — esto es la propuesta, no un compromiso de fecha.
- Fase A — cadena DRx sola, en hardware real. Z1→Z2→Z3 tal como ya están planeados (P-10/11/12 en marcha). Sin diseño nuevo.
- Fase C0 — DSP real contra RCP real, fuente sintética (sin hardware). HECHA, 28-sep-2026. No estaba en el plan original del 17-sep; se intercaló al ver que no dependía de esta placa. Ver la entrada de arriba.
- Fase B — DSP real contra tráfico real de esta placa, selección de vector local/manual.
Completar Z4 apuntado al binario real de DSP (no al stub);
crates/ingestde ese proyecto validado contra cadencia/jitter/contrapresión reales en vez de su simulador sintético. Selección de qué vector reproducevector_source: recarga de DDR/SD + rearranque, o registro AXI-Lite por JTAG — sin RCP ni contrato nuevo todavía. - Fase C (ahora C1) — RCP real contra ese DSP real, alimentado por esta placa. Con C0 hecha,
lo que queda de esta fase es exactamente sustituir el emisor sintético por la ZedBoard.
Literalmente: apuntar el DSP a la placa en vez de al
drx_faker.adapters/dsp/wire.py/moment_stream_receiver.pydel RCP contra el DSP real (cierra el resto de PEND-RCP-05); Level-II y feed RDA/ORPG del RCP ejercitados con radiales de cadencia/jitter reales, no sintéticos (estresa PEND-RCP-12). Ver PEND-RCP-14 del RCP: elstatus/bite_eventde este contrato no llega hoy a ningún sitio del lado RCP. - Fase D — selección de señal de prueba desde el RCP, como función BITE permanente del
operador. Decisión del usuario (2026-09-17): esto no es una herramienta de desarrollo
desechable, es una capacidad de auto-prueba del operador, útil también en comisionamiento de
campo con hardware real. Diseño propuesto, pendiente de ADR propio antes de tocar el
schema (mismo criterio que D-08 para el contrato v0.1 — esto es un cambio de formato, sube
version_minor):- DRx (dueño del contrato): mensaje nuevo
down, p. ej.test_stimulus_select—vector_id(u16, catálogo en SD/DDR) yrepeat(bool). Reutiliza el catálogo de degradaciones que ya existe entools/gen_l1_vectors.py::_cases()(ruido, offset DC, desbalance I/Q, saturación) en vez de inventar uno nuevo. - DSP: relay puro del
commandde su mensajecontrol(dsp_rcp, ya tiene el campo extensible) hacia este mensaje nuevo — no toca el pipeline de moments. - RCP: panel nuevo en la MMI (BITE self-test / inyector de señal), arma el catálogo y dispara.
- Esto convierte el catálogo de vectores de
gen_l1_vectors.pyen producto, no solo en fixture de test — al escribir el ADR, revisar si eso cambia algo de su mantenimiento.
- DRx (dueño del contrato): mensaje nuevo
- Fase E — red y resistencia con hardware real. Throughput real sobre el PHY 1GbE contra la
estimación de
## Presupuesto de 1GbEde este documento (hoy es papel); soak/endurance bajo jitter real, no simulado. - Fase F (stretch, no bloqueante) — sincronizar los dos dobles de la antena.
radar_emulator(Modbus, lo que mueve/lee el RCP) y el emulador SSI en fabric de esta placa (lo que tagea az/el este proyecto) son dos simuladores independientes de la misma antena física, sin verdad compartida. Un HIL completo con "el RCP mueve, el DRx tagea correcto" pide un puente entre los dos. No bloquea A-E: la cadena de señal se valida con az/el fijos en el emulador SSI.
P-14 · Varianza de los estimadores de momento bajo el residuo aceptado (Doviak & Zrnic cap. 6)¶
Estado: abierto, identificado 2026-09-19 · Dueño: especialistas de dominio · Bloquea: nada hoy — es deuda de validación sobre una decisión ya tomada (D-12), no una condición de cierre de Z1.
D-12 acepta el residuo de cuantización del NCO (hasta ±5 LSB por muestra, medido contra xsim real en P-11) como techo del bloque, en vez de exigirle bit a bit. Esa aceptación se apoya en que el residuo "es la misma clase y magnitud" que el proyecto ya acepta desde P-10 — un argumento de semejanza con un caso previo, no una evaluación de su efecto sobre los productos finales del radar.
Queda sin construir el análisis que sí respondería eso: caracterizar la varianza de los estimadores de momento (reflectividad, velocidad Doppler, ancho espectral) que resulta de inyectar este residuo en la cadena, contra el marco teórico de Doviak & Zrnic, Doppler Radar and Weather Observations, cap. 6 (estadística del retorno del radar meteorológico — varianza de estimadores en función de SNR, número de pulsos integrados M y ancho espectral). Sin ese análisis, "la misma clase de residuo que ya se acepta" es la mejor evidencia disponible hoy, pero es indirecta: no dice si ±5 LSB de residuo mueve la varianza de un estimador de velocidad o de reflectividad de forma perceptible para el M real del proyecto.
Nota de honestidad: esta entrada describe qué falta responder, no la respuesta — no hay acceso al texto de Doviak & Zrnic en este repo ni en esta sesión. Cualquier resumen de lo que dice específicamente el capítulo 6 debe venir de quien tenga el libro a mano, no inferirse aquí.
Condición de cierre: un especialista de dominio, con el capítulo 6 (o equivalente) a mano, construye el modelo de varianza del estimador para los M/SNR relevantes del proyecto y lo compara contra el residuo ya medido en P-11. Si la varianza inducida es despreciable frente a la varianza propia del estimador (la que ya existe sin ningún residuo de cuantización, solo por M finito), P-14 cierra sin tocar nada más. Si no lo es, reabre la pregunta de D-12 y de si Taylor —o dithering, la vía que P-11 dejó sin explorar del todo (falta el modelo de integración de M pulsos sobre el ruido de fase, ver esa entrada, 9-sep-2026)— sigue siendo la elección correcta.
P-15 · El mensaje config del contrato solo tiene 1 PRF/ancho de pulso, no 2 lotes¶
Estado: CERRADO 1-oct-2026 (Z4.4) · Dueño: cerrado · Bloquea: nada.
Actualización 22-sep-2026: el mapa de registros AXI-Lite de Z2 (v3 de
config/registers.toml, ver registros.md#z2-en-v3)
ya existe — TIMING_CTRL/TIMING_STATUS/PRF_DIV_/PULSE_WIDTH/N_PULSES/TRIG_DELAY_/TRIG_WIDTH_*,
alimentando drx_timing_engine ya instanciado en drx_top.sv. Eso era la mitad de la condición de
cierre de abajo. La otra mitad — el contrato — sigue intacta, a propósito: D-08 exige discutir
cualquier cambio y subir version_minor con tests actualizados, y ya existía consenso de que ese
lado es alcance de Z4, no de la sesión que diseñó el registro. No se tocó contract/schema/
drx_dsp_v0_1.toml sin esa discusión.
D-13 fija que el motor de timing de Z2 secuencia 2 lotes de PRF/ancho de
pulso por radial (Batch mode de NEXRAD). El modelo dorado (model/drx_model/timing.py) ya modela
eso: Batch + RadialBatches(low, high), con el validador de D-09 aplicado a
cada lote por separado.
Pero contract/schema/drx_dsp_v0_1.toml (congelado en Z0 por D-08) sigue
declarando un único par en el mensaje config: un solo prf_div (u32) y un solo
pulse_width_idx (u8). Eso modela el par global que el "Trabajo" original de Z2 daba por hecho
— exactamente el supuesto que D-13 acaba de descartar. Con el esquema de hoy no hay dónde meter el
segundo lote: ni el DSP puede pedir una configuración de 2 lotes, ni el DRx puede reportar cuál de
los dos produjo un rayo dado.
No se toca ahora porque es alcance de Z4: el contrato es la única interfaz externa, y D-08 dice que
cambiarlo sube version_minor y exige actualizar los tests — una decisión para discutir, no para
tomar de paso al escribir el registro que la motiva. (Hasta el 22-sep-2026 tampoco existía todavía
el registro real que le habría dado la forma concreta; ya existe, ver la actualización de arriba.)
Condición de cierre: config gana los campos para 2 lotes (como mínimo:
prf_div/pulse_width_idx/n_pulses por lote, ya con la forma concreta que fija
config/registers.toml v3 — ver la actualización de arriba — y el metadato por rayo de
Z4 gana qué lote produjo ese rayo) y sube version_minor. Hasta
entonces, cualquier prueba de aceptación de D-09/D-13 contra el contrato real (no contra el modelo
dorado) queda bloqueada por este hueco.
Cierre 1-oct-2026, contrato v0.6 (Z4.4). Las dos mitades de la condición de cierre, hechas:
configpasa a declarar los dos lotes, con la forma concreta que ya teníaconfig/registers.tomlv3 y la estructuraDrxTimingConfigdel firmware:prf_div_low/prf_div_high,pulse_width_idx_low/pulse_width_idx_high,n_pulses_low/n_pulses_highyrange_bins_low/range_bins_high(D-15). El mensaje crece de 48 a 60 B. Se rompe el mensaje anterior en vez de añadir uno nuevo al lado:configestá definido como atómico, y mantener una variante de un solo lote modelaría un modo de operación que D-13 descartó.- El metadato por rayo gana qué lote lo produjo: bandera
batch_high(valor 32) en el enumray_flag. Es bandera y no campo porqueprf_div,binsypulse_width_idxde la cabecera derayya llevan los valores vigentes de ese rayo; el bit sólo dice de cuál de los dos lotes salieron, que es lo que hace falta para agrupar rayos por lote sin reconstruir el radial.
version_minor sube de 5 a 6 y los tests se actualizaron, como exige D-08.
El test que faltaba poder escribir —una configuración de 2 lotes que atraviesa el cable entera y
llega a la acción con los dos lotes distinguidos— es
test_los_dos_lotes_de_prf_llegan_enteros_a_la_accion en fw/tests/test_control.py.
Este cambio se hizo sin esperar a una revisión conjunta con el equipo DSP, bajo la autorización explícita que vladimir dio el 26-sep-2026 para el contrato durante Z4 («trabajá con libertad respecto al contrato, luego actualizamos a DSP»), mismo patrón que el puente PS7 del 20-sep. Queda como aviso pendiente de dar: cualquier implementación que el equipo DSP hubiera empezado contra v0.5 tiene que migrar.
P-16 · El DDS Compiler no tiene aclken: una fuente con huecos mezcla a la frecuencia equivocada¶
Estado: ABIERTO, rebajado 4-oct-2026 · Dueño: Z5 / portabilidad ZU9 · Bloquea:
nada hoy. Ya NO bloquea el barrido del catálogo: el 4-oct-2026 se eliminó la fuente con huecos
—Throttled(DDR) salió del diseño y el estímulo pasa a cargarse en la BRAM de vector_source, que
entrega una muestra por ciclo— así que el síntoma desaparece. La causa raíz sigue en el RTL.
Leer hasta el final: el título de este hallazgo cambió dos veces porque su alcance real resultó
ser mayor que el de su primera redacción, y lo que queda abierto no es lo que decía el título
original.
rtl/vector_source.sv en Throttled(DDR) llena un conjunto de N_CH muestras tomando una por beat de
un AXI4-Stream de 16 bits y lo presenta durante un ciclo, así que el s_valid que ve
drx_ddc_top está alto como mucho 1 de cada N_CH+1 ciclos. Toda la cadena del DDC avanza solo con
valid, de modo que con N_CH = 3 una muestra decimada sale cada ≈4·R₁ ciclos en vez de cada R₁.
drx_range_gate corta donde se lo dice ray_start, que llega cada prf_div ciclos de reloj
(D-14). Un rayo de bins_for_pri(prf_div) bins necesita por tanto unas cuatro veces el PRI para
llenarse, ray_start lo trunca antes, GATE_TRUNCATED sube y no se completa ningún rayo. No es un
fallo de ninguna de las dos piezas: cada una hace lo que su cabecera dice. Es que la combinación no
está contemplada.
El modo sigue siendo útil para lo que Z4.3 lo usó —probar que el camino DDR → MM2S → PL entrega bytes, y ejercitar el plano de control del contrato— y el nombre ya avisaba («Throttled», tasa por debajo de tiempo real, ver fuente de muestras). Lo que no se había dicho en ningún sitio es que eso impide producir rayos con el motor de timing en marcha, que es la combinación que hace falta para barrer los ocho vectores del catálogo contra rayos reales.
Condición de cierre (escrita el 1-oct, superada el 3-oct). Se planteaban dos salidas: que el
DMA entregue N_CH muestras por ciclo —un stream de N_CH·16 bits en vez de 16, cambiando
tcl/gen_zynq_bd.tcl, el camino DDR de vector_source.sv y el entrelazado de
fw/src/app/drx_stimulus.c, las tres a la vez o el reparto por canal se desplaza sin error
visible— o aceptar que el catálogo elegible en caliente solo sirve sin el motor de timing. La
primera tiene además un pero que no estaba anotado: N_CH × 16 = 48 bits no es potencia de dos y
el AXI DMA de Xilinx solo acepta potencias de dos, así que serían 64 bits con un carril muerto.
3-oct-2026: cerrado por una tercera vía que ninguna de las dos contemplaba — sincronizar el
motor con el avance de muestras. El problema no es que el DMA vaya lento; es que las dos mitades
miden el tiempo con reglas distintas. Si drx_prf_batch_seq cuenta AVANCES DE MUESTRA en vez de
ciclos de reloj, las dos se frenan con el mismo factor y el rayo se llena con los mismos
prf_div / R₁ bins a cualquier tasa. En tiempo real valid está siempre alto, así que un avance
es un ciclo y el motor es el de v3..v9 -- no por argumento, por construcción: con advance
constante a 1 la síntesis colapsa el enable.
Piezas, con el criterio de tocar lo mínimo del diseño principal:
rtl/drx_sample_pacer.sv(nuevo): define qué cuenta como un avance --valid && readyen TODOS los canales a la vez, no en uno suelto. Esa definición es la decisión entera, y por eso tiene módulo y L0 propios en vez de ser tresassigndentro dedrx_top.- Un puerto
advanceendrx_prf_batch_seq.svydrx_trigger_one_shot.sv, paso a través endrx_trigger_gen.svydrx_timing_engine.sv. Es la única intrusión en el motor, y no se puede evitar: elenableque ya existe NO sirve de paso porque borrawidth_cnt, así que gatearlo con el tick dejaríatx_gateen pulsos de un ciclo. - Mapa de registros v10:
TIMING_CTRLbit 1,timing_paced, reset 0. Detrás de un bit y no siempre activo a propósito: en BRAM-loopadvanceesready, y si el DDC aplicara contrapresión alguna vez eso movería las cifras de Z4.4, que están cerradas y medidas.
Un detalle que casi se escapa y habría sido un fallo silencioso: pulse_trig es combinacional
sobre pri_cnt == '0'. Gateando solo los always_ff, pri_cnt se queda en 0 durante todos los
ciclos sin avance y pulse_trig sale ANCHO -- y lo consumen ray_start (D-14) y TRIG_COUNT,
que contarían varias veces el mismo pulso de PRF. El advance entra también en ese assign.
Verificación. sim/tb/tb_drx_timing_paced.py compara la traza paceada, muestreada solo en los
ciclos de avance, contra la del mismo motor sin pacear: dos radiales completos, 1804 avances,
(pulse_trig, batch_sel, tx_gate) muestra a muestra. El oráculo aquí no es el modelo dorado sino
el motor viejo, porque lo que falta por probar no es la corrección del secuenciador —ya la cubre
tb_drx_prf_batch_seq.py— sino la EQUIVALENCIA de las dos bases de tiempo. Los dos tests se
comprobaron contra RTL mutado: quitar advance del assign da «10 pulsos con el motor parado»,
quitarlo de los always_ff hace que la traza diverja en el avance 134.
Lo que esto NO cierra. Throttled(DDR) sigue sin ser una fuente de tiempo real, y el nombre ya lo decía. Cierra el barrido del catálogo contra rayos reales. No valida nada dependiente de la tasa -- rayos/s, caudal del transporte, temporización del anillo del DMA --; eso se sigue midiendo en BRAM-loop. Y el validador D-09 comprueba PRF × rango en ciclos: con el motor paceado el PRF real se estira, así que el check queda conservador (nunca rechaza de más) pero deja de hablar del PRF real. Una configuración paceada no es una configuración de tiempo real validada.
P&R: cierra en WNS = 2,394 ns, WHS = 0,013 ns, mejor que los 2,304 ns de Z4.5 — el pacer no añade camino combinacional a la ruta crítica, solo clock-enables sobre registros que ya existían.
La medida en placa, el mismo día: el pacing del motor era necesario pero NO suficiente.
Con el bitstream paceado cargado, tools/sweep_catalogo.py contra los ocho vectores. Lo que
salió bien, que no es poco:
GATE_TRUNCATED = 0x0con el motor corriendo yCTRL = 0x2(DDR). Esa era la firma de P-16 y ya no está: los rayos se completan,bins=3689enteros,flagssinTRUNCATED.- La autoprueba de arranque corre
tono_ifytono_sobre_escalapor DDR con el motor de timing en marcha, dos pasadas cada uno,errores=0. Eso era imposible por definición antes. - Y el camino de tiempo real no se mueve: en BRAM-loop, con
timing_paced = 0, el rayo sigue dando −9,03 dBFS contra −9,03 dBFS del modelo. La equivalencia «conadvancea 1 esto es v9» queda comprobada en silicio y no solo por construcción.
Lo que salió mal, y es el fondo del asunto: hay una TERCERA base de tiempo y no la miré.
rtl/drx_ddc_mixer.sv:99 assign nco_ready = 1'b1; // "el DDS corre libre"
tcl/gen_ip.tcl:238 CONFIG.Has_ACLKEN {false}
El DDS Compiler avanza un paso de fase por ciclo de reloj. No tiene TREADY
(Has_TREADY {false}, decisión razonada de Z1) y no tiene clock enable, así que nada lo ata al
ritmo de las muestras. Con la fuente entregando 1 de cada ~4 ciclos, entre dos muestras
consecutivas la fase ha avanzado 4 pasos: el DDC mezcla a ~4× el IF, el tono cae fuera de la
banda de paso del diezmado y la salida se va al suelo de ruido. Medido: −87,3 dBFS contra los
−9,02 dBFS del modelo, con el pico en un bin arbitrario. Los tres vectores que llegaron a
medirse dieron lo mismo.
El análisis que justificó el pacer —«las dos mitades se frenan con el mismo factor»— era correcto para el motor de timing y el range-gate, y estaba incompleto: el NCO es una tercera mitad. El pacer arregla la LONGITUD del rayo; el DATO necesita que el NCO cuente muestras también.
Camino de arreglo, con su coste. El XCI ya expone el parámetro
(ip/dds_compiler_nco/dds_compiler_nco.xci, hoy "ACLKEN": "false"): regenerar la IP con
Has_ACLKEN {true} y cablear aclken = advance, el mismo advance del pacer. Es simétrico con
el resto y con advance a 1 no cambia nada. Dos cosas que hay que resolver con él:
aclkencongela el core ENTERO, incluido el canalS_AXIS_CONFIGpor el que entra la palabra de AFC (v9). Una escritura que caiga en un ciclo sin avance se perdería.drx_ddc_topya tiene el mecanismo: difiere la escritura mientras el core está en reset; hay que extenderlo a «mientras no haya avance».- El gate real del cambio es la matriz de 16 corridas de D-12 contra xsim, igual que cuando
la IP se regeneró con
Phase_Increment {Programmable}en Z4.5. Son 2-3 h con tres en paralelo, y no se puede saltar: cambiar la personalización de la IP es exactamente el caso que esa matriz vigila.
Y una segunda causa, independiente, que el barrido también destapó: ver P-21.
4-oct-2026: rebajado, no cerrado. Se eliminó la fuente con huecos en vez de arreglar el NCO.
Ninguna de las dos cosas que hacían falta para el arreglo simétrico —regenerar la IP con
Has_ACLKEN {true} y extender el diferido de S_AXIS_CONFIG— se hizo. Lo que se hizo fue quitar
el motivo por el que había huecos:
- Throttled(DDR) sale del diseño. El canal MM2S del AXI DMA, el puerto externo
m_axis_vec, la mitad DDR dertl/vector_source.svyCTRLbit 1 desaparecen del RTL y del block design. - BRAM-loop pasa a ser cargable en tiempo de ejecución. El PS escribe los bancos de
vector_sourcepor la ventanaVEC_ADDR/VEC_DATA/VEC_LENdel mapa v11 (registros.md). El catálogo sigue costando ocho mensajes y ahora además sale a una muestra por ciclo. timing_pacedse queda en 0. Con la fuente a tasa plena, ciclos y avances de muestra son lo mismo y el pacer colapsa en la síntesis. El bit y el módulo se quedan: siguen siendo la respuesta correcta para una fuente lenta, y no cuestan nada con el bit a 0.
Por qué esta salida y no la simétrica. La simétrica cuesta la matriz de 16 corridas de D-12 (2–3 h) y toca una IP ya verificada; ésta no toca el DDC en absoluto, así que la matriz no hace falta, y además desbloquea el barrido del catálogo, que es lo último que le queda a ZM4.
Qué queda abierto, exactamente. El DDS Compiler sigue sin aclken. Cualquier fuente futura
que no entregue una muestra por ciclo —un ADC real con huecos, el JESD204B de la ZU9 si su
valid no es continuo, un modo RT a tasa reducida— vuelve a mezclar a la frecuencia equivocada,
con el mismo síntoma: salida en el suelo de ruido, sin ningún error ni bandera. En la ZU9
diagnosticarlo cuesta mucho más que aquí. El camino de arreglo y sus dos pegas siguen escritos
arriba, intactos, porque siguen siendo válidos.
Condición de cierre, tercera versión. O bien la IP se regenera con Has_ACLKEN {true},
aclken = advance, el diferido de S_AXIS_CONFIG extendido a «mientras no haya avance», y la
matriz de 16 de D-12 en verde; o bien se decide por escrito que la SSA garantiza una muestra por
ciclo en todas las plataformas, y entonces esto deja de ser un hallazgo y pasa a ser un
requisito de interfaz en fuente de muestras. Lo que no
vale es dejarlo como está sin decir cuál de las dos.
P-17 · Sin presupuesto de latencia para la ISR del S2MM¶
Estado: abierto, encontrado 2-oct-2026 (bring-up de Z4.5) · Dueño: Z5 · Bloquea: nada hoy en ZedBoard. Sí el dimensionado de la ZU9 y la campaña de soak.
Nadie ha medido cuánto tiempo puede estar el Cortex-A9 sin atender la interrupción de fin de
descriptor del S2MM antes de perder un rayo. El anillo de fw/src/app/drx_ray.c tiene
DRX_RAY_BUFFERS buffers; mientras la ISR no corra, nadie los devuelve al DMA, y cuando se agotan
el range-gate desborda (STATUS (overrun)). Ese número —el margen en milisegundos— no está escrito
en ningún sitio ni se deriva de ningún parámetro: hoy es un efecto lateral de cuántos buffers se
reservaron.
El hallazgo salió por accidente y con el instrumento equivocado. Durante el bring-up de Z4.5,
xsct detiene el A9 en cada mrd, y una ráfaga de 300 lecturas lo dejó parado ~7 s: el anillo se
secó y aparecieron STATUS (overrun) = 0x7 donde 600 s de marcha ociosa no daban ninguno (detalle
en fases.md, entrada del 2-oct noche). Eso en sí no es un riesgo
de producción —no habrá un depurador leyendo DDR en la ZU9— pero demuestra que el margen existe,
que es finito y que nadie sabe cuánto vale.
Por qué importa más en la ZU9 que aquí, y esto es inferencia, no medida: con dos FMC213 el caudal hacia DDR es mucho mayor, así que el mismo número de buffers cubre menos tiempo. Y el candidato realista a robar ese tiempo no es un depurador sino el propio sistema — una sección crítica larga, una tarea de prioridad alta, el transporte de Z4.6 copiando un rayo entero, o la pila de red con las interrupciones deshabilitadas.
Condición de cierre: un número medido, no estimado — cuántos milisegundos de ISR bloqueada
aguanta el anillo a PRF nominal, obtenido deshabilitando interrupciones a propósito durante un
intervalo creciente hasta ver el primer overrun, y comparado con el que predice
DRX_RAY_BUFFERS × PRI. Con eso, o se dimensiona DRX_RAY_BUFFERS contra un presupuesto declarado
de latencia de interrupción, o se declara el presupuesto y se verifica en el soak de 72 h de Z5.
P-18 · En el borde de lote, un rayo puede llevar la cabecera de uno y la carga útil del otro¶
Estado: CERRADO por consecuencia 3-oct-2026, encontrado el mismo día (soak de Z4.6) ·
Dueño: Z4.6 · Bloquea: nada. Desapareció al sacar lwip_send de la tarea de rayos; la
evidencia y la condición de reapertura, al final de la entrada.
Medido en placa, con las cifras del rayo que lo provocó:
rayo sin enmarcar idx=356735 trig=357002 bins=1002 n_ch=3 n_words=11067 esperadas=3006 batch=1
11067 = 3689 × 3 es exactamente un rayo del lote bajo. La cabecera en banda dice batch=1
y bins=1002 (lote alto, 3006 palabras), y detrás viene la carga útil de un rayo de 3689 bins.
Las dos mitades del mismo rayo describen lotes distintos.
Por qué pasa, hasta donde llega la evidencia. El metadato en banda lo pone drx_ray_packer con
la configuración vigente cuando empieza el rayo, y el conteo de bins con el que drx_range_gate
corta viene del registro del lote (D-15). En la frontera entre el lote bajo y
el alto de un radial (D-13) hay una ventana en la que el metadato ya refleja
el lote nuevo y el gate todavía está vaciando un rayo del viejo. Que los descartes caigan siempre
junto a saltos grandes de trig_count (d = 4, 8, 14, 22, nunca d = 2) y nunca en marcha estable
apoya esa lectura. Es inferencia a partir de la correlación, no una captura del instante: no se ha
mirado con ILA.
El soak de 2 h (1 608 220 rayos) lo caracterizó mejor, y en dos puntos cambia la lectura. Cuatro eventos:
idx=356735 bins=1002 batch=1 n_words=11067 esperadas=3006
idx=904728 bins=1002 batch=1 n_words=11067 esperadas=3006
idx=1140286 bins=3689 batch=0 n_words=3006 esperadas=11067 <- al revés
idx=1434559 bins=1002 batch=1 n_words=11067 esperadas=3006
- Pasa en los dos sentidos (
largos=3, cortos=1), no solo «el gate va por detrás». Eso descarta las explicaciones de retardo fijo en una dirección. - La carga útil es siempre un rayo COMPLETO del otro lote —11067 o 3006 exactos, nunca un número parcial—. Eso descarta truncamiento del DMA o un TLAST a destiempo: el gate entregó un rayo entero y bien formado; lo único equivocado es la cabecera que lo acompaña.
Frecuencia: 4 en 1,6 millones de rayos, ~1 cada 30 minutos a 223 rayos/s.
Y un fenómeno mayor alrededor, que probablemente sea el de verdad. trig_gaps llegó a 20 en
esas 2 h, y la deriva entre trig_count y ray_index creció en 212 disparos a lo largo de
1 077 824 rayos: unos 212 pulsos de PRF que no produjeron ningún rayo, en 20 ráfagas de ~11
pulsos cada una (~50 ms). ray_gaps se mantuvo en 0 todo el tiempo, así que el empaquetador no
perdió el hilo: sencillamente no hubo rayo que empaquetar para esos disparos. El rayo con la
cabecera cambiada aparece en el borde de algunas de esas ráfagas, lo que sugiere que el desajuste
de metadato es un efecto secundario de la ráfaga, no la causa. Qué detiene la producción de
rayos durante ~50 ms, 20 veces en 2 horas, no está medido.
Lo que NO es: corrupción de datos que llegue al DSP. DrxRayFrame_Encode compara n_words
contra bin_count × n_ch con != y se niega a enmarcar el rayo, precisamente para que un rayo de
longitud equivocada no desincronice al lector en el mensaje SIGUIENTE. El rayo se pierde, se cuenta
(long_rays, enc_fail, sink_drops) y el DSP ve un hueco de seq. El coste es un radial con un
rayo menos cada media hora, no un dato malo.
Condición de cierre: que el metadato en banda y el corte del gate vengan del mismo instante, de forma que no exista ventana — o, si se demuestra que la ventana es inherente, que el rayo afectado se marque y se entregue con su longitud real en vez de descartarse. Lo primero es lo correcto; lo segundo solo si lo primero resulta caro. Antes de elegir hace falta mirar el borde con un ILA, que es lo único que convierte la correlación de arriba en una captura.
3-oct-2026: desaparece al sacar la red de la tarea de rayos. Era el efecto secundario, y la
ráfaga era la causa. La sospecha del párrafo anterior —que el desajuste de metadato cuelga de la
ráfaga de ~50 ms y no al revés— quedó medida al corregir lo que paraba la producción: el sumidero
de rayos llamaba a lwip_send dentro de la tarea de rayos, así que mientras el socket bloqueaba
nadie reciclaba el anillo S2MM y el range-gate desbordaba. Con el pool de tramas y la tarea de TX
dedicada (954579c), en 3 085 111 rayos y 3 h 50 min:
dma_overruns=0 trig_gaps=0(d=0) ray_gaps=0 cortos=0 largos=0 enc_fail=0
A la tasa medida antes (4 en 1 608 220) cabía esperar ~7,7 eventos en esa población, y salieron
0. Bajo Poisson eso es p ~ 4,5e-4, así que no es una corrida afortunada. Y el conteo cubre los
3,08 M de rayos enteros, no solo los que se enviaron: short_rays/long_rays se cuentan en
drx_ray.c sobre cada rayo que sale del DMA, haya cliente o no.
Estado: cerrado por consecuencia, no por haber mirado el borde con un ILA. La ventana que la
hipótesis describía puede seguir existiendo en el RTL; lo que ya no existe es la condición que la
hacía visible. Se reabre si largos o cortos vuelven a moverse con el transporte sano — y esa es
toda la vigilancia que necesita.
P-19 · El pool de transmisión pierde ~25 rayos por millón con el host sano¶
Estado: CERRADO 3-oct-2026 (mismo día), como artefacto de medida del cliente de prueba · Dueño: Z4.6 · Bloquea: nada. El control está al final de la entrada.
Es el residuo que queda tras sacar lwip_send de la tarea de rayos (P-18, 954579c). El
sumidero toma un hueco del pool de 4 tramas sin bloquear; si no hay, cuenta queue_full y descarta.
Eso es deliberado: vale más perder un rayo que parar el anillo S2MM. Lo que no está medido es por
qué el pool se queda sin hueco con todo sano.
La medida, y lo que refuta. Durante el soak de 3 h 50 min la máquina de desarrollo sufrió una
tormenta de desasociaciones de WiFi (816 eventos desde las 05:00:13), y la lectura de partida fue
que un host ocupado no vacía el socket, la ventana TCP se cierra y la tarea de TX bloquea.
Corrida de control de 15 min con la WiFi sana —1 desconexión en todo el arranque, 0
failed to send h2c command, carga 0,36—:
| rayos enviados | descartes en el pool | tasa | |
|---|---|---|---|
| soak con tormenta de WiFi | 1 215 129 | 151 | 124 ppm |
| control con host sano | 201 082 | 5 | 25 ppm |
La tormenta explica el exceso, no el suelo. La hipótesis del host ocupado queda en pie para el factor 5, y refutada como explicación completa.
Forma del evento: los 5 descartes produjeron un solo hueco de seq en el cliente, o sea
fueron consecutivos. Con 4 huecos de pool y 223,4 rayos/s, perder 5 seguidos son ~22 ms en los que
nadie vació el pool. No es una pérdida dispersa, es una parada corta y aislada: una en 15 minutos.
Candidatos, por verosimilitud, ninguno medido:
- Parada en el host.
tools/mock_dsp.pyes Python y reensambla ~44 kB por rayo; una pausa de recolección de basura de decenas de ms deja de vaciar el socket el tiempo justo. Es el candidato barato y el primero que hay que descartar, porque si es esto no hay nada que arreglar en la placa. - Agotamiento momentáneo de pbufs en lwIP. El pool de la pila se queda sin buffers y
sendbloquea hasta que los ACK liberan. - La tarea de diagnóstico tomando el mutex de TX para su
statusde 40 B en el peor momento. Encaja mal con la frecuencia observada (1 evento en 15 min contra 1statuspor segundo), pero no está descartado.
Condición de cierre: distinguir entre (1) y el resto. Un cliente que solo haga recv a un
buffer fijo, sin reensamblar ni asignar memoria, durante el mismo tiempo: si los descartes
desaparecen, era el host y P-19 se cierra como artefacto de medida. Si se mantienen, hay que mirar
los contadores de lwIP.
Cerrado el mismo día: era el cliente. El control corrió 900 s contra la misma placa, con el
único cambio de un cliente que hace recv_into sobre un buffer fijo de 256 kB y cuenta bytes:
| rayos enviados | descartes en el pool | |
|---|---|---|
tools/mock_dsp.py (reensambla ~44 kB por rayo en Python) |
201 082 | 5 |
cliente mínimo, solo recv_into |
201 032 | 0 |
Misma duración, misma tasa (28,9 Mbps), mismo firmware, cola clavada en 156 antes y después. El
candidato (1) era el correcto y los otros dos quedan sin evidencia que los sostenga. Estado:
cerrado como artefacto de medida del cliente de prueba, 3-oct-2026.
Lo que esto NO dice: que el pool no se pueda llenar nunca. Dice que a 223,4 rayos/s, con un
consumidor que vacía el socket sin pausas, no se llena. El DSP real tiene su propia latencia de
proceso, y queue_full sigue siendo el contador que hay que mirar cuando aparezca.
P-20 · first_after_config existe en el contrato y el firmware nunca lo pone¶
Estado: abierto, encontrado 3-oct-2026 (al escribir el conductor del barrido) · Dueño: Z4.6 · Bloquea: nada funcional. Sí la solidez de cualquier medida que tenga que saber a qué configuración pertenece un rayo.
RayFlag.FIRST_AFTER_CONFIG (valor 8) está en el esquema desde Z0 y DrxRayContext tiene el
campo, pero fw/src/app/drx_ray_ctx.c lo escribe siempre a false:
out->first_after_config = false;
Nadie lo había necesitado hasta ahora porque nada cambiaba de configuración en caliente. Con el
catálogo de estímulos elegible por mensaje (Z4.3) y el barrido de P-16, sí hace falta: tras un
test_stimulus_select hay rayos en vuelo que todavía llevan el vector viejo, y el DDC tarda en
vaciar su tubería. Sin la bandera, tools/sweep_catalogo.py tiene que decidirlo por tiempo
(--settle, 2 s por defecto), que es una heurística y no una medida.
Por qué importa más de lo que parece. Un --settle corto da un falso FALLO (se midió el
vector viejo). Uno largo no da un falso ACIERTO —el vector nuevo ya suena— pero alarga el barrido
y, lo que es peor, convierte un fallo real de conmutación en un PASS: si el firmware no llegara a
cambiar de vector, un --settle generoso no lo distinguiría de uno que sí cambió, porque el
criterio no mira la configuración sino el reloj.
Condición de cierre: que drx_ray_ctx.c ponga first_after_config en el primer rayo que
empieza después de aplicar una configuración —cualquiera: config, afc,
test_stimulus_select— y que el conductor lo use en vez del temporizador. El mismo enganche que
ya aplica apply_at_seq entre rayos en drx_ray.c es donde va.
P-21 · El MM2S se atasca tras unas pocas selecciones de estímulo¶
Estado: aparcado 4-oct-2026, no cerrado · encontrado 3-oct-2026 (primer barrido del
catálogo) · Dueño: Z4.6 · Bloquea: nada. El camino que lo provocaba ya no existe: el
mismo 4-oct-2026, más tarde, el canal MM2S del AXI DMA salió del block design y la mitad DDR de
rtl/vector_source.sv salió del RTL (P-16). El estímulo se carga ahora por la ventana
AXI-Lite del mapa v11 y no hay ningún DMA que se pueda atascar.
Eso NO lo cierra. El bug no está arreglado ni entendido: está inalcanzable. Si el MM2S vuelve
—un modo RT desde DDR, el puente de la ZU9— vuelve esto con él, y entonces hay que empezar por la
evidencia de abajo y no desde cero. La sonda rtl/drx_debug_probe.sv y el ILA de
tcl/gen_ip_debug.tcl se quedan en el árbol sin instanciar por el mismo motivo.
Encadenando test_stimulus_select, el canal MMSS del AXI DMA deja de aceptar pasadas:
drx_stimulus: vector 2 (dos_tonos), repeat=0
drx_stimulus: pasada sin completar tras 50 ms; sus descriptores quedan en el hardware
...
drx_stimulus: no se pudo encolar la primera pasada
La mitad diagnosticada. DrxStimulus_Stop() pausaba el DMA ANTES de drenar:
XAxiDma_Pause() corta la búsqueda de descriptores a mitad de pasada, así que esa pasada nunca
completa y reap_pass() —que solo devuelve al anillo lo que el hardware marcó como hecho— no
recupera sus BD. El anillo tiene sitio para DRX_STIMULUS_BD_RING / DRX_STIMULUS_BD_COUNT = 4
pasadas, o sea que a la cuarta fuga XAxiDma_BdRingAlloc falla para siempre. No se puede
arreglar con XAxiDma_Reset: MM2S y S2MM son el MISMO axi_dma del block design y resetearlo se
llevaría por delante el anillo de rayos. Corregido drenando antes de pausar, con tope de 50 ms.
La mitad que queda, y que el arreglo convirtió en medible. Con el drenado puesto, el mensaje nuevo dice que la pasada no completa en 50 ms. Una pasada son 4000 conjuntos y el DDC debería tragárselos en menos de 1 ms aunque vaya a 1 de cada 4 ciclos, así que no es lentitud: la cadena se para de verdad en algún punto entre el MM2S y el DDC. Las dos primeras selecciones drenan bien y a partir de la tercera no, lo que sugiere que el atasco se produce durante el bucle normal y no al parar.
Por qué no se había visto. Nadie había encadenado selecciones —Z4.3 probó el modo DDR con el
motor de timing PARADO y con repeat finito— y hasta el pacing de P-16 el síntoma quedaba
tapado: el motor seguía contando ciclos aunque no llegara una sola muestra, así que lo que se veía
era GATE_TRUNCATED y no una parada.
Condición de cierre: saber qué para la cadena.
El ILA ya está construido, 3-oct-2026. rtl/drx_debug_probe.sv + rtl/drx_debug_pkg.sv
(nuevos) sacan a un bus los handshakes de toda la cadena —de s_vec_* a m_ray_*, con
vs_valid/vs_ready/ddc_valid/gate_valid/gate_ready por canal— y cuentan los ciclos desde
el último avance de muestra. tcl/build.tcl -debug 1 engancha un ILA a ese bus; la build normal
no cambia (el XCI vive en ip_debug/, fuera del glob de ip/, y sin DRX_DEBUG_ILA la síntesis
se lleva la sonda entera salvo el contador). Verificado en L0 contra RTL mutado: desplazar un
campo del troceado un bit, o dejar que el contador cuente con armed en bajo, hacen fallar el
banco. Runbook completo, con la tabla de lectura de la captura, en
docs/implementacion/ila.md.
La parte no obvia es el disparo. El umbral de parada son 2²⁰ ciclos ≈ 16,78 ms (tiene que estar por
encima de los huecos legítimos del sondeo de DrxStimulusTask, que desde el 3-oct-2026 es de 1 ms;
con los 10 ms originales hacía falta 2²² ≈ 67 ms y eso tapaba cualquier atasco más corto), y la
ventana del ILA son 4096 muestras = 65 µs: disparar con stalled y guardar todo dejaría el buffer entero dentro del
silencio. Con almacenamiento calificado por actividad (C_EN_STRG_QUAL), esas 4096 muestras son
los últimos ciclos en que algo se movió.
Hipótesis de partida, anotada antes de medir. reap_pass() llama a
XAxiDma_BdRingFromHw(ring, XAXIDMA_ALL_BDS, …), que puede devolver MENOS descriptores que los
DRX_STIMULUS_BD_COUNT de la pasada, y aun así pone g_in_flight = false y declara la pasada
entera hecha. Dentro del bucle de drenado de DrxStimulus_Stop() eso corta el drenado antes de
tiempo: XAxiDma_Pause() deja el descriptor que faltaba en el hardware y set_modo_ddr(false)
baja ddr_mode, con lo que s_axis_ready de vector_source se va a 0 y ese descriptor ya no
puede completarse nunca. Es el mismo agujero que el arreglo de arriba cerró por un lado y esto
reabre por otro, y explica el «a partir de la tercera». Predicción falsable: la captura tiene
que mostrar s_vec_valid = 1, s_vec_ready = 0 y ddr_mode = 0 en el mismo ciclo. Si ddr_mode
sigue en 1, la hipótesis es falsa. El firmware se deja sin tocar hasta tener la captura: arreglar
la hipótesis antes de medirla cerraría P-21 por coincidencia.
Las lecturas de MM2S_DMASR por JTAG de la primera sesión dieron patrones de bits implausibles
(errores de SG en un canal que estaba entregando rayos) y no se usaron como evidencia.
Bloqueo de hardware de la primera sesión, ya resuelto con un apagado y encendido. Al cargar el bitstream de
depuración apareció otro fallo delante: el AXI DMA no mueve nada en NINGUNO de los dos canales,
con los dos corriendo (RS = 1), con descriptores pendientes y sin un solo bit de error. El
ILA lo dejó claro en una sola foto de 4096 ciclos idénticos — m_ray_valid = 1 con
m_ray_ready = 0, s_vec_valid = 0, y la PL moviéndose por dentro
(vs_valid = vs_ready = 0b111) —, o sea que lo parado es el DMA y no la lógica. El primer
arranque fallido es el siguiente a reprogramar la PL con el S2MM transmitiendo a 225 rayos/s:
una transacción colgada en HP0, del lado PS, donde no llegan ni la reconfiguración de la PL ni
ps7_init. No se recupera por software — probadas cuatro recargas y un ciclo explícito de
FPGA_RST_CTRL. Hace falta apagar y encender la placa. Detalle y la regla que sale de esto
("parar el camino de rayos antes de reprogramar la PL") en
docs/implementacion/ila.md.
Lo que sí deja demostrado esa captura: el instrumento funciona, y la contrapresión de la cadena
se comporta como está diseñada (gate_valid = 0b111 contra gate_ready = 0b000, overrun
subiendo).
La hipótesis está REFUTADA, y lo que se midió es otra cosa (3-oct-2026, tarde). Con la placa
reiniciada el instrumento se usó de verdad. Primero un control positivo —parar el MM2S a mano
(MM2S_DMACR bit 0 a 0) dejando ddr_mode en alto— que disparó en el instante de la escritura
con idle_cnt = 1048576, o sea 2²⁰ clavado: el contador, el flanco y el decodificador quedan
verificados contra silicio. Después, el A/B del sondeo, mismo bitstream y mismo barrido
(--settle 1) en las dos patas:
| Firmware | Selecciones encadenadas | Disparos |
|---|---|---|
| sondeo 1 ms, tick 1 kHz | 80 | ninguno |
| sondeo 10 ms, tick 100 Hz | 24 | 2, a los 32 s y a los 16 s |
Las dos capturas del lado de 10 ms dan ddr_mode = 1, s_vec_valid = 0, s_vec_ready = 1,
vs_ready = 0b111, overrun = 0b000. La predicción exigía s_vec_valid = 1 con
ddr_mode = 0: falsa. No hay un beat atascado que vector_source rechace; hay hambre — el
consumidor listo y el productor ausente durante más de 16,78 ms. El arreglo de reap_pass() que
se había dejado sin tocar a propósito ya no tiene por qué ser la causa.
Lo que NO cierra esto, y por eso P-21 sigue abierto. El atasco permanente del hallazgo
original no reprodujo hoy en ninguna de las dos patas: las 24 selecciones del lado de 10 ms
terminaron todas con config_ack error=ok y un rayo de vuelta, dma_overruns=0,
trig_gaps=0. Lo medido es un hueco transitorio del que la cadena sale sola. Que el sondeo de
10 ms produzca huecos de alimentación está demostrado; que sea además la causa del atasco
permanente es plausible y no está demostrado.
Condición de cierre, segunda versión (3-oct-2026): reproducir el atasco permanente con el ILA armado. Si no vuelve a aparecer en una corrida larga con el sondeo de 1 ms, cerrar por consecuencia y decirlo así, como se hizo con P-18.
El atasco permanente SÍ reprodujo, tres veces, y lo que destapa es que hay dos modos de fallo
distintos (3 y 4 de octubre de 2026). Tres corridas desatendidas con el mismo bitstream, el
mismo firmware de 1 ms y el mismo orquestador tools/noche_p21.sh, cada una sobre una placa
recién encendida con SW8 — el atasco es permanente por diseño del firmware, así que una corrida
equivale exactamente a un atasco:
| corrida 1 | corrida 2 | corrida 3 | |
|---|---|---|---|
| tiempo hasta el atasco | 7 min 42 s | 56 min 18 s | 62 min 14 s |
| selecciones encadenadas | 323 | 2837 | 3141 |
| pasadas del catálogo | 41 | 354 | 392 |
| líneas precursoras | 7 | 0 | 0 |
| disparos naturales del ILA | 4 | 0 | 0 |
Las corridas 2 y 3 coinciden entre sí dentro del 10 % y sus capturas son idénticas columna por columna. La corrida 1 es ocho veces más corta y su estado final es el opuesto. No son muestras de la misma distribución, y promediarlas daría una cola larga que el dato no respalda.
- Modo A, corrida 1 — la cadena se queda llena y bloqueada.
m_ray_valid = 1conm_ray_ready = 0(el S2MM no acepta),gate_valid = 0b111contragate_ready = 0b000,idle_cntsaturado en0xffffff. Los cuatro disparos naturales llevan calificación de almacenamiento sobremoved, y en los 4095 avances de muestra que guardan,ddr_modevale 0 todas las veces. De ahí la inferencia, fuerte: en esa corrida la cadena no avanzó ni una sola muestra conddr_mode = 1— el camino DDR no entregó nada nunca, en vez de entregar unas cuantas y atascarse después. - Modo B, corridas 2 y 3 — la cadena se queda vacía y drenada. El inverso exacto:
m_ray_valid = 0conm_ray_ready = 1(el S2MM sí acepta y no hay nada que darle),gate_valid = 0b000,s_vec_valid = 0cons_vec_ready = 1. No hay contrapresión desde el consumidor; no llega dato desde el MM2S.
El instrumento no sirve tal como está, y esto es lo primero que hay que arreglar si se retoma.
stalled no disparó ni una vez en el modo B: 118 minutos acumulados de cadena parada entre las
corridas 2 y 3, y cero flancos. Las únicas capturas de ese modo son las que forzó el vigía con
run_hw_ila -trigger_now. Sin él no habría quedado nada.
Incógnita viva: algo resetea idle_cnt cada pocas décimas de milisegundo durante el atasco del
modo B. En las fotos forzadas el contador vale 11223 y 20864 ciclos, tomadas entre 45 y 49
segundos después del mensaje de atasco; sin reseteos llevaría del orden de 3·10⁹ ciclos y
estaría saturado, como en el modo A. drx_debug_probe.sv solo resetea con !armed o moved, y
dentro de las ventanas capturadas ddr_mode vale 1 y moved vale 0. Quedan dos explicaciones, y
el instrumento actual no las distingue: o el MM2S entrega una muestra suelta de vez en cuando —o
sea el atasco es un goteo y no una parálisis, lo que explicaría además por qué stalled nunca
llega a 2²⁰— o ddr_mode cae durante algún ciclo. La primera explica las dos observaciones a la
vez, pero con dos puntos de dato es hipótesis, no hallazgo.
La inversión de causalidad NO se escribe como hallazgo. Que el S2MM deje de aceptar antes de que el MM2S se quede sin alimentar descansa sobre la corrida 1 sola, y las corridas 2 y 3 la contradicen. El modo A no volvió a aparecer en dos horas de intento.
Las capturas de las tres corridas están versionadas en
docs/implementacion/evidencia/p-21/, con la lectura
completa. El arnés (tools/noche_p21.sh, tools/cargar_dbg.sh, tools/triaje_p21.sh,
tools/ila_p21.tcl, tools/ila_p21_leer.py) se queda en el repositorio, listo para volver a
correr.
Por qué se aparca en vez de cerrarse, 4-oct-2026. El estímulo va a pasar a cargarse en BRAM en tiempo de ejecución y a alimentar el DDC desde ahí, con lo que el MM2S deja de estar en el camino de datos y no se le encuentra hoy otro uso. Un bug que nadie puede provocar no merece más noches de medida. Pero no está arreglado ni entendido: no se sabe qué para la cadena en ninguno de los dos modos. Si el MM2S vuelve al diseño por cualquier motivo, P-21 vuelve con él, y entonces esta entrada y las capturas son el punto de partida, no el final.
Lo que haría falta para retomarlo, en este orden: cambiar la condición de disparo del ILA
—stalled con umbral 2²⁰ no la ve—, discriminar entre el goteo del MM2S y la caída de ddr_mode
instrumentando las dos, y solo entonces volver a correr. El arreglo pendiente de reap_pass()
(la reaparición parcial de descriptores) sigue sin aplicarse a propósito: la refutación de la
hipótesis de partida lo debilita como causa, pero no lo descarta.
Detalle, las tres trampas de recargar solo el ELF por JTAG y la regla de que una recarga en caliente no es comparable con un arranque en frío, en docs/implementacion/ila.md.
P-22 · Ningún vector del catálogo ejercita el overflow del DDC¶
Estado: cerrado como inalcanzable por estímulo, 5-oct-2026 (encontrado 4-oct-2026 en el primer barrido completo del catálogo que llegó a comparar dato) · Dueño: — · Bloquea: nada. Queda un hueco de verificación aceptado, no un fallo.
Decisión de cierre, 5-oct-2026. La condición de cierre original (más abajo) no se puede
cumplir: con la ganancia de Z1, ninguna entrada de 16 bits hace desbordar el DDC. Medido contra
el modelo dorado con tools/bound_ddc_overflow.py: la cadena es lineal salvo redondeos, así que la
salida máxima posible con |x| <= fs es fs · Σ|g|, con g la respuesta medida con un impulso en
cada posición de entrada y barriendo las 25 fases del NCO.
| Modo | Salida máxima posible | Margen hasta saturar |
|---|---|---|
| fino (R=208) | 0,927 fs | 0,65 dB |
| grueso (R=416) | 0,832 fs | 1,6 dB |
Un vector construido con el signo de esa respuesta, o sea el peor caso, dio |pico I| = 30057 y
overflow = 0/64 en modo fino. Los redondeos mueven unos pocos LSB, no el 7 % que falta. La pega
anotada abajo se queda corta: no es que subir la amplitud no llegue, es que ningún contenido
espectral llega. Un vector distinto no sirve; habría que cambiar la ganancia de Z1, y eso toca
D-12 y su matriz de 16 corridas por una verificación de BITE.
Lo que queda sin verlo en placa: el cableado de overflow desde drx_saturate por
drx_ddc_quad_top, el sticky de drx_axil_regs, el registro OVERFLOW (v2) y la bandera
ddc_overflow de la trama ray. L0 cubre drx_saturate, el sticky y el registro con estímulo
inyectado. Que el cableado entre ellos funcione en silicio es inferencia, no medida. Hoy el
hardware no puede levantar la bandera, y sweep_catalogo.py la compara contra el modelo, así que
detectaría un ddc_overflow espurio. Si alguna vez hace falta verlo levantado: un gancho de
prueba en RTL (bit de CTRL o desplazamiento de ganancia en drx_saturate), con registro v12, L0
con mutación, P&R y medida en placa. El WNS de partida es 1,618 ns. El mismo problema reaparece si
una plataforma futura sube la ganancia hasta Σ|g| >= 1: ahí el vector sí existiría.
El catálogo trae tono_sobre_escala, un tono a +6 dBFS que tools/gen_l1_vectors.py:129
describe así: «Entrada sobre-escala: el ADC real satura, no envuelve. Ejercita el…» reporte de
overflow. tools/sweep_catalogo.py exigía en consecuencia que ese vector, y solo ese, levantara
ddc_overflow en las banderas del rayo.
Medido en placa el 4-oct-2026: no lo levanta, y no debería. El rayo da −4,32 dBFS, y el modelo dorado da exactamente −4,32 dBFS. Preguntado al modelo directamente, para los ocho vectores y en los dos modos de celda:
fine tono_sobre_escala overflow = 0/19 |pico| = 21005 (fondo de escala 32767)
coarse tono_sobre_escala overflow = 0/9 |pico| = 462
La entrada está a +6 dBFS, pero el ADC la recorta a fondo de escala antes de entrar al DDC, y
el ajuste de ganancia fina de Z1 deja el pico de salida 3,9 dB por debajo del fondo. No hay nada
que saturar. El hardware y el modelo coinciden; lo que estaba mal era la expectativa escrita a
mano en la herramienta, puesta antes de que nadie midiera — y Z4.3 ya lo había anotado como tal
en fw/include/drx_stimulus.h: «Se informa, no se exige: que un vector concreto llegue a saturar
la salida del DDC depende del ajuste de ganancia de la cadena y no está medido en placa
todavía». Ya está medido.
Corregido el mismo día: sweep_catalogo.py compara la bandera contra lo que el modelo predice
(dump_ray.model_channel devuelve ahora overflow), no contra una lista de nombres de vector.
Con eso el barrido cierra 8/8. La corrección es la correcta en los dos sentidos: también
detectaría un ddc_overflow espurio, y sigue valiendo el día que alguien añada un vector que sí
desborde.
Lo que queda abierto, que es lo que importa. Con el catálogo de hoy, nada ejercita el camino
de overflow del DDC de punta a punta en silicio: ni la salida overflow de drx_saturate, ni su
propagación por drx_ddc_quad_top, ni el latch sticky de drx_axil_regs, ni el registro OVERFLOW
(v2), ni la bandera ddc_overflow de la trama ray. L0 cubre el registro y el sticky con un
estímulo inyectado; lo que no se ha visto nunca es a la FUENTE real levantarlo.
Condición de cierre original (inalcanzable, ver arriba): un vector del catálogo para el que el modelo dorado prediga
overflow > 0, barrido en placa, y que la bandera llegue hasta la trama ray. Y hay una pega que
conviene decir antes de que alguien lo intente subiendo la amplitud: no se llega por ahí. La
entrada ya está recortada a fondo de escala por el ADC, así que subir los dBFS del tono no cambia
lo que entra al DDC. Haría falta un estímulo cuyo contenido espectral concentre la ganancia de la
cadena en la banda de paso — inferencia, no medida; el diseño concreto de ese vector es parte del
trabajo.
P-23 · Cargar un vector cuesta un rayo: el motor de timing no se entera¶
Estado: cerrado como coste aceptado, 5-oct-2026 (encontrado 4-oct-2026 en el primer soak encadenando barridos del catálogo) · Dueño: — · Bloquea: nada. No llega dato malo al DSP. Es un coste conocido, no una corrupción.
Decisión de cierre, 5-oct-2026: no se arregla. Nadie necesita barrer el catálogo sin perder un
rayo, y el arreglo obvio empeora el modo de fallo. Inferencia, no medida: retener el motor con
timing_advance & ~vector_load congelaría el rayo en curso a mitad, y al soltar la carga se
completaría con tamaño y cabecera correctos pero mitad vector viejo y mitad nuevo.
DrxRayFrame_Encode no lo rechazaría, así que cambiaría un rayo perdido y visible por uno
empalmado y silencioso hacia el DSP. Hoy el rayo de la frontera se trunca y Encode se niega a
enmarcarlo, que es el comportamiento seguro. Si alguna vez hace falta cero huecos: la puerta
más un clear del range-gate al cerrar la carga, con P&R, L0 con mutación y medida en placa.
Cada test_stimulus_select pone CTRL bit 2 (vector_load) y lo mantiene mientras el PS escribe
las VECTOR_LOOP_LEN muestras por VEC_DATA. Con vector_load en alto, rtl/vector_source.sv
congela el lector a propósito — es lo que impide que medio vector viejo y medio nuevo suenen a la
vez. Pero el motor de timing no sabe nada de eso: con timing_paced = 0 cuenta ciclos de
reloj, así que ray_start sigue disparando sobre una cadena sin muestras.
Medido: 201 barridos encadenados, 41 min, 1608 selecciones más 3 del arranque.
drx_status: rayo trig_gaps=1684(d=2) cortos=68 largos=65 enc_fail=114
dma_err=0 ray_gaps=0 magic=0 n_ch=0
drx_status: BITE=0x0 uptime=2480s rayos=553495 dma_overruns=0 ddc_overflows=0
ssa_underruns=0 last_error=0
1684 / 1611 = 1,045 huecos de trigger por selección. El resto cuadra con la aritmética: la carga son 4000 escrituras AXI-Lite contra PRIs de 12,28 ms (lote bajo) y 3,33 ms (lote alto), así que la ventana de congelación abarca aproximadamente un disparo. La latencia por escritura no está medida, así que el factor exacto es inferencia; el 1,045 sí es medida.
Lo que NO pasa, y es lo que importa: dma_overruns = 0, ray_gaps = 0, ddc_overflows = 0,
BITE = 0x0, y 223,2 rayos/s sostenidos (553 495 rayos en 2480 s) contra los 223,47
predichos. Los 68 cortos y 65 largos son rayos de la frontera de la carga —cabecera de una
configuración y carga útil de otra, la misma clase de evento que
P-18— y los 114 enc_fail son DrxRayFrame_Encode negándose a enmarcarlos, que es
exactamente para lo que esa comprobación existe. No llega dato malo al DSP.
sink_drops, cola y snd_fail salen altos (20 386 / 20 272 / 203) porque el barrido conecta y
desconecta 1608 veces; son artefactos de interacción con el cliente y no se usan como criterio de
salud, igual que en las corridas de Z4.6.
Condición de cierre: que vector_load también retenga el motor de timing, de modo que una
carga no consuma PRIs. El enganche natural es el mismo advance de rtl/drx_sample_pacer.sv
(mapa v10): poner timing_paced = 1 durante la carga lo conseguiría sin RTL nuevo, porque sin
muestras no hay avances y el motor se para solo — inferencia, no medida, y hay que comprobar
que el cambio de base de tiempo a mitad de radial no deje ese radial con dos PRIs distintas, que
es justamente lo que drx_regs_hw.c evita hoy parando el secuenciador antes de tocar el bit.
Alternativa más limpia y más cara: un puerto de retención en drx_timing_engine que congele el
secuenciador sin cambiar lo que prf_div significa.
Por qué no se arregló al encontrarlo: cuesta un rayo cada 1608 PRIs durante un barrido de banco de pruebas, y cero en operación normal —nadie recarga vectores con el radar funcionando—. Arreglarlo toca el motor de timing, que está cerrado y medido desde Z2. No vale la pena hasta que alguien necesite barrer el catálogo sin perder un solo rayo.
P-24 · El signo del NCO del AFC solo está validado en la ZedBoard, no en la ZU9¶
Estado: abierto, 8-oct-2026 (encontrado al cerrar Z4.6) · Dueño: — · Bloquea: el comisionado del lazo de AFC en la ZU9. No bloquea Z4.6 ni ZM4.
Qué se sabe (medido, ZedBoard): el mezclador del DDC está conjugado. Subir nco_phase_inc
sube el pico del espectro, así que el DSP manda NCO = rx_if − freq_hz (palabra absoluta de
32 bits; 15 MHz a 62,5 MHz son 1030792151). Con esa convención el lazo cierra: pico del canal 2 de
−0,416 a 0,000 de fs_dec, −9,03 dBFS, afc_freq_meas_hz y afc_control_freq_hz a 2 Hz tras
90 s. Con el signo contrario el lazo se alejaría del pico en vez de acercarse.
Qué NO se sabe: si el DDC de la ZU9 conjuga igual. Es el mismo RTL (inferencia: no se
comparó el netlist ni se midió en la ZU9), pero el signo depende de cómo se cablea el DDS
Compiler a cada plataforma, y la ZedBoard va dilatada ×4 en el tiempo (fs_dec ≈ 150,5 kHz
medida), lo que enmascara cualquier diferencia que solo aparezca a la frecuencia real.
Cómo cerrarlo cuando llegue el hardware: (1) con el DSP parado, barrer el NCO ±40 kHz con
tools/mock_dsp.py --afc y comprobar con --spectrum hacia dónde se mueve el pico del canal 2;
(2) si coincide con la ZedBoard, correr el DSP real 90 s y comprobar pico en 0 y afc_bite = 0;
(3) si el signo es el contrario, el cambio es una línea en crates/service/src/main.rs
(rx_if − freq por rx_if + freq), no en el firmware. Con la ZU9 sin dilatar, LAMULA_DSP_FAST_TIME_DILATION
vuelve a 1.0.
Otros puntos sin cubrir del mismo cierre: rango de captura del estimador ±0,5 ciclos/muestra (≈ ±75 kHz en la ZedBoard; un desvío mayor se interpreta como alias), y no se midió el desfase de rayos en vuelo entre la corrección aplicada y la medida (despreciable con ganancia ≈ 0,035, inferencia).
P-25 · Tras una caída de enlace, el DRx tarda en servir al cliente nuevo si el socket viejo sigue abierto¶
Estado: cerrado como limitación aceptada, 12-oct-2026 (encontrado en la inyección de fallos de Z5) · Dueño: — · Bloquea: nada. Se reabre si: el DSP real no cierra su socket al detectar la caída, o si ZM5 exige recuperación acotada tras un flap de enlace.
Medido (ZedBoard, cable desenchufado a mano): con el cliente conectado durante una caída de
23 s, el primer rayo llegó 21 s después de volver el enlace. Con el cliente cerrando su socket
antes de la caída, 0,5 s. Con una caída de 12 s, 0 rayos en los 8 s siguientes. Evidencia (locales,
no versionadas) en build/fallos/20261012-212211-enlace, 20261012-212028-enlace y
20261012-212906-enlace-limpio.
Mecanismo (inferencia del código, no medido con un tracer): drx_transport.c:283-327 serializa
lwip_accept y atiende(fd) en una tarea; mientras el socket viejo no muera, el nuevo espera en
el backlog. lwIP lo cierra solo cuando una retransmisión (TCP_MAXRTX 12, backoff exponencial)
recibe respuesta del host, y keepalive está apagado (LWIP_TCP_KEEPALIVE 0,
lwipopts.h:56 del BSP). La duración depende de en qué punto del backoff vuelve el enlace.
No se sabe: el peor caso (en teoría, minutos); si la duración crece con la de la caída (dos
puntos medidos, no una curva); ni la causa de los 127 rayos/s de una ventana posterior a la
reconexión en la corrida enlace-limpio (222,6/s cinco minutos después, sin datos por segundo en
medio).
Cómo reproducir: tools/inyectar_fallos.py enlace (cliente conectado durante la caída) y
enlace-limpio (cierra el socket antes). Pide desenchufar y enchufar el cable a mano: sin sudo no
se puede bajar enp1s0 por software.
Cómo arreglarlo, si se reabre, de menos a más coste: (a) activar SO_KEEPALIVE en el socket
del cliente con ~10 s de inactividad y subir LWIP_TCP_KEEPALIVE a 1 en el BSP (tcl/gen_bsp.tcl
fija los parámetros de lwIP a mano con bsp config), lo que acota el peor caso a ~30 s sin tocar
el control de flujo; (b) lwip_select sobre el cliente y srv para que una conexión nueva expulse
a la vieja, más SO_SNDTIMEO para que el lwip_send de drx_tx no cuelgue contra un peer muerto.
(b) toca el camino de rayos: recarga de placa y soak corto de regresión.
P-26 · link_down del BITE no se levanta¶
Estado: cerrado como limitación aceptada, 12-oct-2026 · Dueño: — · Bloquea: nada. Se
reabre si: el catálogo de fallos de ZM5 exige BITE LINK_DOWN visible.
drx_status_task.c:125 difiere link_down a Z4.6 y se reporta a cero; el transporte existe desde
Z4.6 y nadie lo cableó. Medido: BITE limpio y last_error=ok en cuatro caídas reales de enlace
(tools/inyectar_fallos.py enlace). mmcm_unlocked y ssi_errors están igual de a cero, por
razones distintas (no hay bit en STATUS; encoders de Z3), y eso sí está dicho en
drx_status_frame.h.
Cómo cablearlo, si se reabre: hay que leer el estado del enlace del netif de lwIP (lo sabe el
PS, no el fabric), ponerlo en la instantánea de drx_status_task.c junto a ssa_underruns, y
decidir si es sticky o de nivel. Un bit de nivel es lo que un DSP necesita para saber si el enlace
está caído ahora, pero el status solo llega por ese mismo enlace, así que un DSP conectado
nunca vería link_down=1 en vivo: solo el valor sticky tras volver. Por eso el criterio del
catálogo («BITE lo reporta») solo es comprobable por UART, no por el socket. Esa limitación es de
diseño, no del cableado, y conviene decidirla antes de implementar.