ELAI S.r.l.

El sensor ha terminado: ¿por qué la IA sigue leyendo la muestra anterior?

Un modelo ejecutado distingue lecturas antiguas, sobrescrituras y pérdida de bytes vecinos: CPU y DMA deben coordinar toda la línea de caché.

El sensor ha terminado: ¿por qué la IA sigue leyendo la muestra anterior?

Un sensor adquiere una nueva ventana de datos. La transferencia a memoria termina y el programa inicia la inferencia. Sin embargo, el resultado parece corresponder a la ventana anterior. Antes de sospechar de los pesos del modelo o de la normalización, debemos plantear una pregunta más elemental: ¿el procesador y el dispositivo que transfiere los datos ven realmente la misma copia? Comprenderemos cómo pueden divergir dos representaciones de una misma dirección y por qué el remedio exige acordar quién posee la memoria durante la transferencia.

Resumen. Estudiamos un modelo didáctico determinista de una línea de caché write-back de 32 bytes. La CPU accede a la copia en caché y el DMA directamente a la RAM, sin un mecanismo automático de coherencia. Siete secuencias ejecutadas distinguen lectura de datos antiguos, sobrescritura de una muestra nueva y pérdida de una modificación de un byte vecino. El resultado central es que hay que razonar sobre la línea completa, no solo sobre los bytes del búfer. El modelo explica un mecanismo: no mide una placa, no simula todos los comportamientos del Cortex-M7 ni proporciona un controlador listo para usar.

Dos caminos hacia una dirección

La CPU es el procesador que ejecuta el programa. La RAM es la memoria de trabajo. Una caché guarda cerca del procesador copias de pequeños bloques de memoria para evitar acceder continuamente a la RAM. El DMA, acceso directo a memoria, puede transferir datos sin que la CPU copie cada elemento. Es útil para muestras de sensores e imágenes, pero introduce un segundo actor con acceso a la memoria.

En nuestro modelo, una lectura de la CPU utiliza la caché si el bloque ya está presente; en caso contrario, lo carga desde la RAM. Una escritura modifica la copia local y la marca como sucia. Sucia significa diferente de la memoria subyacente, no corrupta. Con la política write-back, la modificación puede llegar a la RAM posteriormente. El DMA lee o escribe directamente en RAM. No afirmamos que todos los sistemas con DMA funcionen así: suponemos expresamente que no existe coherencia por hardware entre estos caminos.

Una analogía útil es un cuaderno junto a un registro central. Actualizar el registro no corrige automáticamente una anotación ya copiada. La analogía termina ahí: las transferencias y el mantenimiento del hardware tienen unidades precisas, y no podemos elegir libremente qué dato individual descartar. Por eso pasamos inmediatamente a un modelo explícito.

La muestra 7, la muestra 99 y un vecino

Imaginemos una sola línea con 32 posiciones de un byte, numeradas de 0 a 31. Inicialmente, la RAM contiene 7 en la posición 8 y 1 en la posición 0; las demás posiciones contienen cero. La posición 8 representa una muestra. La posición 0 representa una variable independiente que el programa quiere actualizar a 9. Son valores sintéticos, no lecturas de un sensor ni resultados de un modelo de IA.

La caché comienza vacía. Cuando el procesador lee la posición 8, el modelo copia los 32 bytes de la RAM a la caché. Desde entonces puede leer 7 en su copia local aunque otro actor modifique la RAM. Así se hace visible la granularidad: la muestra y su vecino pertenecen a la misma unidad de mantenimiento.

La línea de 32 bytes corresponde al contexto Cortex-M7 descrito en la nota ST AN4839; no es una propiedad universal de las cachés. El ejemplo usa desplazamientos, no direcciones reales de un microcontrolador. Tampoco supone que un controlador DMA concreto pueda acceder a una región determinada: esa accesibilidad debe comprobarse en el manual del dispositivo elegido.

Primer error: entregar una modificación que sigue en caché

Consideremos primero la dirección CPU hacia DMA. El procesador quiere transmitir 99 y lo escribe en la posición 8. En el modelo, la caché contiene ahora 99 mientras la RAM conserva 7. Si el DMA arranca inmediatamente, lee 7. Para el programa la escritura ya se produjo, pero todavía no está disponible en el camino utilizado por el DMA.

La operación clean escribe en RAM el contenido de una línea sucia. Repitiendo la secuencia con clean antes de la lectura DMA, el resultado pasa de 7 a 99. Estos dos casos aíslan la causa: mismos valores iniciales y misma transferencia, con una sola operación añadida. No demuestran una mejora de rendimiento, porque el programa no representa ciclos, buses ni frecuencias.

No hay que confundir clean con invalidate. En el modelo, invalidate descarta la copia local. Si descartamos la única copia que contiene una modificación útil todavía ausente de RAM, perderemos esa modificación. La pregunta correcta no es qué función parece más tranquilizadora, sino dónde está en ese momento la información que debemos conservar.

Segundo error: recepción completada pero invisible

Invertimos ahora la dirección. La CPU lee inicialmente 7 y conserva una línea válida. El DMA escribe 99 en RAM en la posición 8 y termina. El procesador sigue leyendo su copia y obtiene 7. La transferencia ha funcionado en RAM; el problema es qué copia utiliza la lectura siguiente.

Invalidate, después de completar la transferencia en nuestro orden serial, elimina la copia antigua. La lectura siguiente recarga la línea y puede ver 99. Existen dos necesidades diferentes: hacer disponible una modificación CPU para el DMA e impedir que la CPU reutilice una copia anterior a una modificación DMA. Confundir las direcciones conduce a remedios equivocados.

Hasta aquí hemos supuesto que no existen otras modificaciones que conservar en la línea. Es una hipótesis fuerte, a menudo oculta cuando el problema se describe únicamente como una muestra antigua que eliminar. El byte vecino permite ponerla a prueba.

Por qué limpiar demasiado tarde puede empeorar el resultado

La CPU modifica el vecino de la posición 0, pasando de 1 a 9. Como la escritura carga y ensucia toda la línea en el modelo, la caché también conserva la muestra antigua 7. Después, el DMA escribe 99 en RAM en la posición 8. Si ejecutamos clean en ese momento, toda la línea local vuelve a memoria: se guarda el vecino 9, pero la muestra cambia de 99 a 7.

No hemos leído simplemente una copia antigua: hemos destruido la copia nueva en RAM. Incluso una invalidación posterior hará que la CPU recargue 7, porque 99 ya ha sido sobrescrito. Esta secuencia se llama rx_late_clean en el código. Su resultado muestra por qué clean e invalidate no pueden tratarse como operaciones intercambiables que se añaden al final de una transferencia.

Probar el remedio opuesto revela otro error. En la misma situación, ejecutemos únicamente invalidate después del DMA. La CPU ve correctamente 99, pero el vecino vuelve a 1: la modificación 9 solo existía en la copia descartada. Una prueba que compruebe únicamente la muestra declararía éxito y omitiría una corrupción fuera del búfer.

Un acuerdo que protege toda la línea

En el caso correcto del modelo, la CPU modifica el vecino, ejecuta clean antes de entregar la línea al DMA y no la toca durante la transferencia. La RAM ya contiene 9 cuando el DMA escribe la nueva muestra 99. Tras la finalización, invalidate elimina cualquier copia CPU que haya dejado de ser válida. Las lecturas finales devuelven 99 y 9: se conservan ambas informaciones.

La condición crucial es que la CPU no acceda de forma que pueda volver a ensuciar la línea mientras pertenece al DMA. Lo comprobamos con un séptimo caso: después de preparar la línea, la CPU modifica el vecino durante la fase asignada al DMA; un clean posterior reproduce la sobrescritura. Una preparación previa no hace inocua una violación posterior de la propiedad.

Este acuerdo afecta a la granularidad real de la caché. Asignar ocho bytes al DMA no los separa de los otros veinticuatro que comparten línea. El experimento no prescribe una secuencia universal de llamadas de firmware: muestra qué información debe conservar un protocolo concreto. Una plataforma real necesita reglas adicionales documentadas de orden, finalización y mantenimiento.

¿Qué intervalo se afecta realmente?

Supongamos que el búfer comienza en el desplazamiento a y tiene longitud n, con n mayor que cero. Si cada línea contiene L bytes, queremos todas las líneas que intersectan el intervalo del búfer. Los límites matemáticos son:

inicio = L × floor(a/L)
fin = L × ceil((a+n)/L)
extensión = fin − inicio

Floor redondea hacia abajo y ceil hacia arriba. Todas las cantidades son direcciones o recuentos en bytes; L es el tamaño de línea. Para a=8, n=8 y L=32, el búfer ocupa [8,16), pero el mantenimiento cubre [0,32). La notación [inicio,fin) incluye el extremo inicial y excluye el final. La posición 0 está fuera del búfer, pero dentro de la línea.

Redondear correctamente no resuelve por tanto la compartición: identifica el territorio que debemos controlar. Incluso un búfer con inicio alineado puede compartir su última línea si su longitud no ocupa un número entero de líneas. En C también hay que comprobar desbordamiento de la dirección final, tamaño cero, tipos aceptados por la API y límites de memoria. La fórmula es una relación matemática, no una función lista para pegar en un controlador.

Qué hace el programa reproducible

experiment.py contiene una lista RAM de 32 valores y una copia caché inicialmente ausente. Una lectura CPU rellena la copia solo cuando falta. Una escritura modifica la copia y activa dirty. Clean transfiere la línea sucia a RAM; invalidate elimina la copia y la marca. El DMA modifica o lee la lista RAM sin consultar la copia CPU.

Cada escenario parte del mismo estado inicial y no hereda modificaciones de otro. Cada operación guarda una instantánea numérica de muestra, vecino y marca dirty. Las aserciones comprueban los resultados esperados y detienen la ejecución si las relaciones ilustradas no se cumplen. No hace falta semilla aleatoria porque no hay sorteos. El resultado es determinista, no una estimación estadística de numerosas ejecuciones.

Para repetir el análisis, ejecute Python con experiment.py --out results.json en la carpeta del material. El JSON contiene versión de Python, hipótesis, unidades, intervalo cubierto y cronologías completas. El cálculo no necesita paquetes externos. Inspeccionar valores intermedios es más informativo que mirar solo el resultado final: permite distinguir la pérdida de una modificación CPU de la pérdida de una modificación DMA.

Alternativas y preguntas abiertas

Una opción de diseño separa físicamente las líneas de los búferes de las de otras variables, controlando ubicación y tamaños. Otra utiliza una región no cacheable cuando el dispositivo lo permite. Una tercera copia los datos a un búfer de trabajo después de una entrega coherente. Son estrategias para comparar, no vencedoras universales: ubicación, accesibilidad del DMA y coste de transferencia dependen del sistema.

El modelo no permite asignar milisegundos ni julios a estas alternativas. No representa expulsiones espontáneas, accesos especulativos, interrupciones, concurrencia entre núcleos ni barreras. Su ausencia no permite ignorarlos en firmware: precisamente impide convertir el experimento en una prueba de corrección de un producto. Antes de una prueba física hay que identificar dispositivo y revisión, región de memoria, atributos MPU, controlador DMA, entorno de ejecución y cabeceras CMSIS, junto con el protocolo de prueba.

Respuesta a la pregunta inicial

El sensor puede haber entregado correctamente una muestra nueva y la IA seguir leyendo la anterior porque CPU y DMA no consultan necesariamente la misma copia. Distinguir clean e invalidate aclara cómo hacer visibles las modificaciones en ambas direcciones. El byte vecino añade el punto decisivo: mantener coherente solo la muestra no basta cuando las operaciones afectan a toda una línea.

La enseñanza es diseñar la transferencia como una entrega temporal de memoria con propiedad y granularidad explícitas. En el modelo esto conserva tanto 99 como 9; en una placa real hay que traducir y verificar la misma idea según la documentación de la plataforma. No se realizaron mediciones de hardware ni se demostraron prestaciones o funcionalidades de EL-AI.

Referencias y estado del material

STMicroelectronics, AN4839, revisión 2, marzo de 2018, secciones 2, 2.1, 3.2 y 4, leídas durante la preparación del 11 de octubre: https://www.st.com/resource/en/application_note/an4839-level-1-cache-on-stm32f7-series-and-stm32h7-series-stmicroelectronics.pdf
Arm CMSIS-Core 6, D-Cache Functions, documentación releída el 15 de octubre de 2026; parámetros de operaciones por dirección: https://arm-software.github.io/CMSIS_6/latest/Core/group__Dcache__functions__m7.html
Texto con asistencia de IA, sin afirmar revisión humana. Ejemplos sintéticos propios; ninguna reproducción de benchmark o investigación original revisada por pares.

Resultados sintéticos de los siete casos

SecuenciaMuestra observadaVecino final
tx_missing_clean71
tx_clean991
rx_stale71
rx_late_clean79
rx_only_post_invalidate991
rx_clean_before_invalidate_after999
rx_neighbor_written_during_transfer79
Figura: a la izquierda, clean tardío devuelve la RAM de 99 a 7; a la derecha, una entrega sin accesos CPU conserva 99. Abscisas: índices de operación, no tiempo. Izquierda: escritura del vecino, DMA, clean, invalidate, dos lecturas; derecha: escritura del vecino, clean, DMA, invalidate, dos lecturas. Un hueco en la copia CPU indica caché ausente, no cero. Sin mediciones de hardware.
Figura: a la izquierda, clean tardío devuelve la RAM de 99 a 7; a la derecha, una entrega sin accesos CPU conserva 99. Abscisas: índices de operación, no tiempo. Izquierda: escritura del vecino, DMA, clean, invalidate, dos lecturas; derecha: escritura del vecino, clean, DMA, invalidate, dos lecturas. Un hueco en la copia CPU indica caché ausente, no cero. Sin mediciones de hardware.

Código completo del modelo ejecutado

"""Deterministic teaching model, not a Cortex-M7 emulator or hardware benchmark.
One write-back cache line with 32 byte entries; CPU/DMA operations are serialized.
No timing, barriers, speculative fills, cache sets, bus topology or instruction model.
"""
import json,platform,argparse
from pathlib import Path
class Model:
 def __init__(self):
  self.ram=[0]*32;self.ram[8]=7;self.ram[0]=1
  self.cache=None;self.dirty=False;self.trace=[]
 def snap(self,event):
  self.trace.append(dict(event=event,ram_buffer=self.ram[8],ram_neighbor=self.ram[0],cache_buffer=None if self.cache is None else self.cache[8],cache_neighbor=None if self.cache is None else self.cache[0],dirty=self.dirty))
 def read(self,i):
  if self.cache is None:self.cache=self.ram.copy()
  value=self.cache[i];self.snap('cpu_read_'+str(i));return value
 def write(self,i,v):
  if self.cache is None:self.cache=self.ram.copy()
  self.cache[i]=v;self.dirty=True;self.snap('cpu_write_'+str(i))
 def dma_write(self,i,v):self.ram[i]=v;self.snap('dma_write_'+str(i))
 def clean(self):
  if self.dirty:self.ram=self.cache.copy();self.dirty=False
  self.snap('clean')
 def invalidate(self):self.cache=None;self.dirty=False;self.snap('invalidate')

def scenario(name):
 m=Model()
 if name.startswith('tx'):
  m.write(8,99)
  if name=='tx_clean':m.clean()
  dma=m.ram[8];m.snap('dma_read_8');out={'dma_value':dma,'expected':99}
 elif name=='rx_stale':
  m.read(8);m.dma_write(8,99);out={'cpu_value':m.read(8),'expected':99}
 elif name=='rx_late_clean':
  m.write(0,9);m.dma_write(8,99);m.clean();m.invalidate();out={'cpu_value':m.read(8),'neighbor':m.read(0),'expected':99}
 elif name=='rx_only_post_invalidate':
  m.write(0,9);m.dma_write(8,99);m.invalidate();out={'cpu_value':m.read(8),'neighbor':m.read(0),'expected_neighbor':9}
 elif name=='rx_clean_before_invalidate_after':
  m.write(0,9);m.clean();m.dma_write(8,99);m.invalidate();out={'cpu_value':m.read(8),'neighbor':m.read(0),'expected':99,'expected_neighbor':9}
 elif name=='rx_neighbor_written_during_transfer':
  m.read(8);m.clean();m.write(0,9);m.dma_write(8,99);m.clean();m.invalidate();out={'cpu_value':m.read(8),'neighbor':m.read(0),'expected':99}
 else:raise ValueError(name)
 return dict(name=name,outcome=out,trace=m.trace)

def main(out):
 names=['tx_missing_clean','tx_clean','rx_stale','rx_late_clean','rx_only_post_invalidate','rx_clean_before_invalidate_after','rx_neighbor_written_during_transfer']
 rows={n:scenario(n) for n in names}
 assert rows['tx_missing_clean']['outcome']['dma_value']==7
 assert rows['tx_clean']['outcome']['dma_value']==99
 assert rows['rx_stale']['outcome']['cpu_value']==7
 assert rows['rx_late_clean']['outcome']['cpu_value']==7
 assert rows['rx_only_post_invalidate']['outcome']=={'cpu_value':99,'neighbor':1,'expected_neighbor':9}
 assert rows['rx_clean_before_invalidate_after']['outcome']=={'cpu_value':99,'neighbor':9,'expected':99,'expected_neighbor':9}
 assert rows['rx_neighbor_written_during_transfer']['outcome']['cpu_value']==7
 # A range [8,16) touches cache line [0,32), including unrelated byte0.
 a=8;n=8;line=32;lo=(a//line)*line;hi=((a+n+line-1)//line)*line
 assert (lo,hi)==(0,32)
 r={'python':platform.python_version(),'seed':None,'units':{'address':'byte offsets','values':'synthetic byte values'},'hardware_executed':False,'line_size_bytes':32,'assumptions':'Serialized operations on one initially empty write-back cache line; DMA accesses RAM directly; full-line clean/invalidate; no hardware timing or cache-policy reproduction','range':{'buffer_start':a,'buffer_size':n,'maintenance_start':lo,'maintenance_size':hi-lo},'scenarios':rows}
 Path(out).write_text(json.dumps(r,indent=2),encoding='utf8')
 print(json.dumps({n:v['outcome'] for n,v in rows.items()},indent=2))
if __name__=='__main__':
 p=argparse.ArgumentParser();p.add_argument('--out',default='results.json');main(p.parse_args().out)

Reproducibilidad y complejidad

Copiar una línea de L elementos requiere O(L) operaciones Python y O(L) memoria. Cada escenario contiene un número fijo de accesos. Son costes del modelo didáctico, no ciclos de caché real. Ejecutar experiment.py --out results.json; plot.py requiere Matplotlib y Microsoft YaHei de Windows para chino. No se distribuye la fuente.

Descargar código y resultados · Resultados JSON

Portada ilustrativa generada con IA, no fotografía de un dispositivo o laboratorio EL-AI.