Registro a 8kHz en un ESP32-S3 en Arduino: ejecutar el programa desde PSRAM (XiP)
El IMU del Jupiter se está moviendo a SPI de 10MHz en la próxima placa para poder registrar a 8kHz, subiendo desde los 2kHz que gestiona en I2C hoy. En el Rocketry Forum alguien cuestionó bastante si un ESP32-S3 a 240MHz realmente puede "registrar" a 8kHz, en lugar de simplemente leer el sensor tan rápido. Es una buena pregunta y aunque estaba seguro de que podríamos hacerlo, aún no lo había verificado, así que en lugar de adivinar fuimos y lo medimos correctamente. Esto es lo que encontramos, y en la parte inferior está la biblioteca Arduino reconstruida con la que terminamos, si quieres probarla en tus propios proyectos.
Realmente no es la velocidad del reloj
Lo que detiene la mayoría de proyectos ESP32 registrando rápidamente no es la CPU, es la flash. En una compilación normal el programa se ejecuta directamente desde el mismo chip flash en el que estás registrando, a través de un pequeño caché. Cada vez que escribes en esa flash, el ESP apaga el caché en ambos núcleos hasta que termina la escritura, así que cualquier código que no esté ya sentado en RAM interna simplemente se detiene. No se ralentiza, se detiene. Una escritura de página es alrededor de un milisegundo, y borrar un bloque de 64KB puede congelar todo durante 100 a 200ms. Para una computadora de vuelo ejecutando un bucle de velocidad fija, ese es el problema real.
Qué estamos registrando
Cada muestra del IMU es el giroscopio como tres enteros de 16 bits y la aceleración como tres enteros de 24 bits, los acelerómetros de alto y bajo g fusionados en una lectura, entonces 15 bytes por muestra.
Comenzamos planeando almacenar la aceleración fusionada como flotantes, pero los enteros resultaron ser el mejor ajuste. La unidad es exactamente 1/2048 g, que es el paso propio del acelerómetro de bajo g en su rango de 16g (el 0.488mg de ST es ese número redondeado), así que sus lecturas caen en números enteros y nada de lo que mide el sensor se pierde. Convertirlo de nuevo a g es una división entre 2048, que es exacta, y 24 bits cubre aproximadamente ±4000g, mucho más de lo que necesita el rango de 256g de alto g. Son 3 bytes por eje en lugar de 4, y a diferencia de un flotante, puede ser codificado en delta después si alguna vez queremos (más sobre eso abajo).
Todo lo demás que el Jupiter registra (el estado de vuelo de 500Hz, baro, magnetómetro, GNSS, potencia y demás) suma aproximadamente 20kB/s sin importar la velocidad del IMU, así que cuanto más rápido vayamos, menor es la proporción de datos que representa: 40% a 2kHz, una cuarta parte a 4kHz y solo el 14% a 8kHz.
| Velocidad del IMU | Datos del IMU (kB/s) | Otros datos (kB/s) | Total (kB/s) | Con delta (kB/s) |
|---|---|---|---|---|
| 2kHz | 30 | 20 | 50 | 42 |
| 4kHz | 60 | 20 | 80 | 64 |
| 8kHz | 120 | 20 | 140 | 108 |
La cifra de otros datos es aproximada. La columna con delta es nuestra estimación del total a flash en el impulso, donde la vibración del motor hace que la codificación delta sea menos efectiva, con solo los datos del IMU codificados en delta. En la almohadilla y planeo sería incluso menor. Hay más sobre codificación delta más abajo.
Cómo registra el Jupiter
- El IMU agrupa sus muestras en su propio FIFO, así que el bucle no está haciendo 8000 lecturas por segundo. Se ejecuta a 500Hz y extrae lo que está esperando en una ráfaga en cada pasada, que son 16 muestras por pasada a 8kHz.
- Cada muestra sin procesar entra en búferes de anillo en el PSRAM. Nada se reorganiza, los anillos son bloques de memoria fijos con posiciones de lectura y escritura, y el registrador simplemente camina a lo largo de ellos en el lugar. También significa que los datos previos al lanzamiento ya están allí cuando se detecta el lanzamiento.
- Un pequeño búfer de 4KB en RAM interna recopila los registros y los escribe en una partición flash raw dedicada. Esa partición se borra antes del vuelo, así que en vuelo solo hay escrituras de página, sin borrados y sin sistema de archivos en el camino.
- Después del aterrizaje comprime todo el vuelo en un único archivo, transmitido en flujo para que nunca necesite todo el vuelo en memoria, sea cual sea la velocidad a la que fue registrado.
Todo eso aún se encuentra con el problema de flash anterior, que es de donde viene la siguiente parte.
Ejecutar el programa desde PSRAM
El ESP32-S3 puede copiar el programa en PSRAM al arranque y ejecutarlo desde allí en lugar de desde la flash (Espressif lo llama XIP desde PSRAM). Una vez que lo está haciendo, una escritura de flash ya no necesita que el caché se apague, así que el bucle continúa mientras se escribe en la flash. El problema para los usuarios de Arduino es que es una configuración que no se usa en las bibliotecas precompiladas de Espressif, así que no es algo que puedas activar desde tu sketch, así que reconstruimos las bibliotecas S3 para el núcleo Arduino con eso activado.
ECC, porque los cohetes se calientan
Mientras estábamos en ello activamos ECC para el PSRAM. Espressif califica el PSRAM octal en el S3R8 hasta 65C, y 85C con ECC activado. 65C suena como mucho, pero un cohete oscuro sentado en una almohadilla al sol se calienta sorprendentemente dentro, así que para una computadora de vuelo pareció el lugar sensato para comenzar. Cuesta un dieciséisavo del PSRAM, que tenemos fácilmente disponible.
Vale la pena notar que esta es la clasificación de temperatura ambiente, no la clasificación de temperatura interna del chip. Y como el Jupiter no usa RF (Wifi/BT etc) probablemente se estire un poco más.
Uno más pequeño: dónde vive el búfer de escritura
Encontramos que el búfer de escritura de 4KB también importa. Si está en PSRAM, el controlador flash de ESP-IDF lo copia a través de un pequeño búfer de 32 bytes y escribe cada 32 bytes como un comando separado al chip flash. Si está en RAM interna, se lo entrega directamente, 64 bytes por comando, que es lo máximo que el controlador flash del S3 tomará en un viaje. Cada comando cuesta aproximadamente lo mismo sin importar lo que lleve, así que la mitad de los comandos es casi el doble de velocidad: totalmente medimos 209kB/s desde PSRAM y 335kB/s desde RAM interna. 4KB de RAM interna no es nada, así que ese fue un cambio fácil.
La prueba
Agregamos una prueba al firmware que escribe el búfer de vuelo de la misma manera que lo hace el registrador de vuelo mientras el bucle de 500Hz continúa ejecutándose, luego reporta qué tan mal se vio afectado el bucle. Escribe a 55kB/s, luego 166kB/s, luego a toda velocidad, luego borra todo lo que escribió. Esas dos velocidades se establecieron desde nuestro plan de flotante anterior (18 bytes por muestra) para registro de 2kHz y 8kHz, así que están un poco por encima de lo que el formato entero necesita, lo que hace que la prueba sea un poco más difícil que la vida real. Para aclarar una cosa, el IMU en esta placa aún está en I2C a 2kHz, así que las fases más rápidas son la carga flash de un registro más rápido, no un muestreo más rápido. Mismo tablero, misma prueba, solo las bibliotecas Arduino intercambiadas.
Cada celda es el núcleo Arduino de stock primero, luego nuestras bibliotecas reconstruidas con el programa en PSRAM y ECC activado. El bucle debería ejecutarse a 500Hz, una pasada cada 2ms, y la brecha más peor es el tiempo más largo entre dos pasadas.
| Mientras la flash está | Velocidad del bucle | Peor brecha | Desbordes del FIFO |
|---|---|---|---|
| Escribiendo a 55kB/s | 349 → 499Hz | 15.8 → 5.4ms | 42 → 0 |
| Escribiendo a 166kB/s | 193 → 499Hz | 15.0 → 5.2ms | 77 → 0 |
| Escribiendo a toda velocidad | 196 → 499Hz | 12.9 → 4.6ms | 24 → 0 |
| Borrando (bloques de 64KB) | 17 → 499Hz | 202 → 4.9ms | 3 → 0 |
En el núcleo de stock el bucle se derrumba tan pronto como la flash está ocupada, ni siquiera pudo mantenerse al día con la fase de 166kB/s (logró 160), y el borrado lo detiene durante 200ms. Cada desbordamiento es el FIFO del IMU llenándose y las muestras perdiéndose. Con el programa en PSRAM mantiene 499Hz a través de todo, incluyendo el borrado. La peor brecha de aproximadamente 5ms es solo la pasada más lenta del bucle propio (hace eso sin ninguna escritura flash en absoluto), el FIFO del IMU la absorbe, y nada se pierde.
Codificación delta
La codificación delta significa almacenar cuánto cambió cada lectura desde la anterior en lugar de la lectura misma. A estas velocidades una muestra es muy cercana a la siguiente, así que la mayoría de los cambios caben en uno o dos bytes en lugar de dos o tres. Aquí es donde los enteros ganan su sueldo: el cambio entre dos enteros es en sí mismo un entero pequeño, mientras que los bits de un flotante no se encogen cuando el valor apenas se mueve. En pasos de 1/2048 g, un cambio de hasta aproximadamente 4g entre muestras cabe en 2 bytes.
Cuánto ahorra depende de lo que esté haciendo el cohete. Sentado en la almohadilla o planeando, la mayoría de las muestras caerían a 6 a 8 bytes, pero en el impulso con el motor sacudiendo todo es más como 11, y el impulso es lo que importa ya que ese es donde la velocidad de registro está en su más alta. Esa es nuestra estimación aproximada en lugar de una medición por ahora. El Jupiter no codifica en delta el búfer de vuelo en este momento, ya que no lo necesita para velocidad. Donde ayudaría es registrando tiempo: el búfer de vuelo de 7.8MB tiene aproximadamente 58 segundos a 8kHz raw completo, u aproximadamente 75 segundos codificados en delta, antes de que el registro híbrido reduzca la velocidad.
¿Entonces qué tan rápido podría registrar?
Juntando las velocidades flash medidas con los tamaños de datos anteriores da lo máximo que cada configuración podría mantener:
| Configuración | Flash, a toda velocidad (kB/s) | Velocidad máxima, raw / delta (kHz) | Datos 8kHz, raw / delta (kB/s y proporción de flash) |
|---|---|---|---|
| Núcleo de stock, búfer en PSRAM* | 160 | 9 / 12 | 140 / 108 88% / 68% |
| Núcleo de stock, búfer en RAM interna* | 306 | 19 / 25 | 140 / 108 46% / 35% |
| Bibliotecas reconstruidas, búfer en PSRAM | 209 | 12 / 17 | 140 / 108 67% / 52% |
| Bibliotecas reconstruidas, búfer en RAM interna (el Jupiter) | 335 | 21 / 28 | 140 / 108 42% / 32% |
* La flash se mantiene al día en el núcleo de stock pero el bucle no. Cayó por debajo de 200Hz y perdió muestras del IMU a cada velocidad que probamos, así que en la práctica no puede registrar limpiamente a ninguna de estas.
Raw es 15 bytes por muestra, codificado en delta es nuestra estimación de 11 bytes de impulso, ambos incluyen el 20kB/s de todo lo demás, y son las velocidades a toda velocidad medidas con el bucle ejecutándose, redondeado hacia abajo sin nada guardado de repuesto. El bus SPI de 10MHz tampoco es el límite en ninguno de estos, podría llevar alrededor de 59,000 muestras por segundo.
Así que 8kHz necesita aproximadamente 140kB/s contra los 335kB/s que la flash toma a toda velocidad, aproximadamente el 42% sin ninguna codificación delta en absoluto, o aproximadamente 108kB/s (32%) con la misma. La CPU tampoco está trabajando tan duro: a 2kHz la mayoría de cada pasada del bucle se gasta esperando en el bus I2C, y en SPI de 10MHz la misma lectura a 8kHz debería tomar alrededor de 0.3ms. El propio IMU se agota a 8kHz, así que el resto es espacio disponible. Aún es prueba de banco hasta que llegue la placa SPI, y los valores por defecto serán más bajos de todas formas, con el registro bajando su velocidad después del impulso, pero el S3 se ve bien equipado para ello.
Pruébalo tú mismo
Como hemos hecho el trabajo duro, las bibliotecas reconstruidas ESP32-S3 están en las descargas en la parte inferior de este post para cualquier otro que use Arduino. Son para el núcleo esp32 3.3.12 del Administrador de Placas solamente, y reemplazan esta carpeta:
Windows %LOCALAPPDATA%\Arduino15\packages\esp32\tools\esp32s3-libs\3.3.12 Mac ~/Library/Arduino15/packages/esp32/tools/esp32s3-libs/3.3.12 Linux ~/.arduino15/packages/esp32/tools/esp32s3-libs/3.3.12
En Windows, cierra el IDE de Arduino, luego abre un Símbolo del sistema (escribe cmd en el menú Inicio, no PowerShell) y pega esto. Encuentra el zip en tu carpeta Descargas sin importar lo que la descarga agregó a su nombre, y mantiene tu carpeta original como 3.3.12.stock:
cd /d "%LOCALAPPDATA%\Arduino15\packages\esp32\tools\esp32s3-libs"
ren 3.3.12 3.3.12.stock && for %f in ("%USERPROFILE%\Downloads\*esp32s3-libs-3.3.12-xip-ecc*.zip") do tar -xf "%f"
rmdir /s /q "%LOCALAPPDATA%\arduino\cores"
La última línea borra la copia en caché del IDE del núcleo compilado para que se reconstruya contra las nuevas bibliotecas. Para volver a stock:
cd /d "%LOCALAPPDATA%\Arduino15\packages\esp32\tools\esp32s3-libs" ren 3.3.12 3.3.12.xip-ecc && ren 3.3.12.stock 3.3.12 rmdir /s /q "%LOCALAPPDATA%\arduino\cores"
No uses Extraer Todo en el Explorador en el zip, algunos de los caminos dentro son demasiado largos y silenciosamente saltará archivos. El comando anterior o 7-Zip están bien.
Algunas cosas a saber:
- Lo hemos probado en un S3 con 8MB de PSRAM octal (el S3R8), con PSRAM configurado en PSRAM OPI en el menú Herramientas. No lo hemos intentado en PSRAM cuádruple, y como el programa se ejecuta desde PSRAM no será adecuado para placas sin ninguno.
- El programa se copia en PSRAM al arranque, así que pierdes esa cantidad de PSRAM, más un dieciséisavo de la misma para ECC.
- Está construido con el generador de bibliotecas esp32-arduino de Espressif en ESP-IDF 5.5.5, lo mismo que las bibliotecas oficiales 3.3.12, con solo dos configuraciones agregadas. Todo el crédito va a Espressif. Es no oficial y tal cual, así que mantén tu carpeta stock.
Si prefieres compilarlo tú mismo, es el esp32-arduino-lib-builder de Espressif (Linux o WSL), extraído en las versiones listadas en el archivo versions.txt en la carpeta de bibliotecas oficiales 3.3.12, con estas dos líneas agregadas a configs/defconfig.esp32s3 antes de ejecutar la compilación:
CONFIG_SPIRAM_XIP_FROM_PSRAM=y CONFIG_SPIRAM_ECC_ENABLE=y
El IMU del Jupiter se está moviendo a SPI de 10MHz en la próxima placa para poder registrar a 8kHz, subiendo desde los 2kHz que gestiona en I2C hoy. En el Rocketry Forum alguien cuestionó bastante si un ESP32-S3 a 240MHz realmente puede "registrar" a 8kHz, en lugar de simplemente leer el sensor tan rápido. Es una buena pregunta y aunque estaba seguro de que podríamos hacerlo, aún no lo había verificado, así que en lugar de adivinar fuimos y lo medimos correctamente. Esto es lo que encontramos, y en la parte inferior está la biblioteca Arduino reconstruida con la que terminamos, si quieres probarla en tus propios proyectos.

