Logging em 8kHz em um ESP32-S3 no Arduino: executando o programa da PSRAM (XiP)
O IMU do Jupiter está migrando para SPI de 10MHz na próxima placa para que possa registrar a 8kHz, acima dos 2kHz que consegue em I2C atualmente. No Rocketry Forum alguém levantou uma questão legítima sobre se um ESP32-S3 a 240MHz realmente consegue "registrar" a 8kHz, em vez de apenas ler o sensor nessa velocidade. É uma pergunta válida e, embora eu estivesse confiante de que conseguíríamos fazer isso, ainda não tinha verificado, então em vez de adivinhar fizemos uma medição apropriada. Isto é o que descobrimos, e no final há a biblioteca Arduino reconstruída que acabamos por usar, se quiser experimentar em seus próprios projetos.
Não é realmente a velocidade do relógio
O que impede a maioria dos projetos ESP32 de registrar rapidamente não é a CPU, é a flash. Em uma compilação normal, o programa é executado diretamente da mesma chip de flash em que você está registrando, através de um pequeno cache. Toda vez que você escreve nessa flash, o ESP desativa o cache em ambos os núcleos até que a escrita seja concluída, portanto qualquer código que não esteja já sentado na RAM interna simplesmente para. Não desacelera, para. Uma escrita de página dura cerca de um milissegundo, e apagar um bloco de 64KB pode congelar tudo por 100 a 200ms. Para um computador de voo executando um loop de taxa fixa, esse é o verdadeiro problema.
O que estamos registrando
Cada amostra do IMU tem o giroscópio como três inteiros de 16 bits e a aceleração como três inteiros de 24 bits, os acelerômetros de baixa e alta g fundidos em uma única leitura, então 15 bytes por amostra.
Começamos planejando armazenar a aceleração fundida como floats, mas inteiros se mostraram o melhor ajuste. A unidade é exatamente 1/2048 g, que é o próprio passo do acelerômetro de baixa g em sua faixa de 16g (o 0.488mg da ST é esse número arredondado), portanto suas leituras caem em números inteiros e nada do que o sensor mede é perdido. Convertê-lo de volta para g é uma divisão por 2048, que é exata, e 24 bits cobrem aproximadamente ±4000g, bem mais do que os 256g que a faixa de alta g precisa. São 3 bytes por eixo em vez de 4, e diferentemente de um float, pode ser codificado em delta depois se quisermos (mais sobre isso abaixo).
Todo o resto que o Jupiter registra (o estado de voo de 500Hz, baro, magnetômetro, GNSS, energia e assim por diante) adiciona cerca de 20kB/s qualquer que seja a taxa do IMU, portanto quanto mais rápido vamos, menor é a parcela dos dados que isso representa: 40% a 2kHz, um quarto a 4kHz e apenas 14% a 8kHz.
| Taxa de IMU | Dados de IMU (kB/s) | Outros dados (kB/s) | Total (kB/s) | Com delta (kB/s) |
|---|---|---|---|---|
| 2kHz | 30 | 20 | 50 | 42 |
| 4kHz | 60 | 20 | 80 | 64 |
| 8kHz | 120 | 20 | 140 | 108 |
A cifra de outros dados é aproximada. A coluna com delta é nossa estimativa do total para flash no boost, onde a vibração do motor torna a codificação delta menos eficaz, com apenas os dados do IMU codificados em delta. Na plataforma e em voo planado seria ainda menor. Há mais sobre codificação delta mais abaixo.
Como o Jupiter registra
- O IMU agrupa suas amostras em seu próprio FIFO, portanto o loop não está fazendo 8000 leituras por segundo. Ele é executado a 500Hz e puxa tudo o que está esperando em um lote a cada passagem, que são 16 amostras por passagem a 8kHz.
- Cada amostra bruta vai para buffers circulares no PSRAM. Nada é mexido, os anéis são blocos fixos de memória com posições de leitura e escrita, e o logger apenas caminha por eles no lugar. Também significa que os dados de pré-lançamento já estão lá quando o lançamento é detectado.
- Um pequeno buffer de 4KB na RAM interna reúne os registros e os escreve em uma partição flash bruta dedicada. Essa partição é apagada antes do voo, portanto em voo são apenas escritas de página, sem apagamentos e sem sistema de arquivos no caminho.
- Depois do pouso, comprime todo o voo em um único arquivo, transmitido para que nunca precise do voo inteiro na memória, qualquer que seja a taxa em que foi registrado.
Tudo isso ainda esbarra no problema da flash acima, porém, que é onde a próxima parte entra.
Executando o programa a partir do PSRAM
O ESP32-S3 pode copiar o programa no PSRAM na inicialização e executá-lo de lá em vez da flash (Espressif chama de XIP do PSRAM). Uma vez que esteja fazendo isso, uma escrita em flash não precisa mais desativar o cache, portanto o loop continua enquanto a flash é escrita. O problema para usuários do Arduino é que é uma configuração não utilizada nas bibliotecas pré-compiladas da Espressif, portanto não é algo que você possa ativar a partir de seu sketch, então reconstruímos as bibliotecas do S3 para o núcleo do Arduino com ela ativada.
ECC, porque foguetes esquentam
Enquanto estávamos nisso, ativamos ECC para o PSRAM. Espressif classifica o PSRAM octal no S3R8 para 65C, e 85C com ECC ativado. 65C parece muito, mas um foguete escuro sentado em uma plataforma ao sol fica surpreendentemente quente por dentro, portanto para um computador de voo pareceu o lugar sensato para começar. Custa um décimo sexto do PSRAM, que podemos facilmente poupar.
Vale notar que esta é a classificação de temperatura ambiente, não a classificação de temperatura interna do chip. E como o Jupiter não usa RF (Wifi/BT etc) provavelmente se estenderá um pouco mais.
Mais um pequeno: onde o buffer de escrita fica
Descobrimos que o buffer de escrita de 4KB também importa. Se estiver no PSRAM, o driver de flash ESP-IDF copia através de um buffer minúsculo de 32 bytes e escreve cada 32 bytes como um comando separado para o chip de flash. Se estiver na RAM interna, ele passa diretamente, 64 bytes por comando, que é o máximo que o controlador de flash do S3 aceitará de uma vez. Cada comando custa aproximadamente o mesmo qualquer que seja o que ele carrega, portanto metade dos comandos é quase o dobro da velocidade: em plena velocidade medimos 209kB/s do PSRAM e 335kB/s da RAM interna. 4KB de RAM interna não é nada, portanto esse foi uma mudança fácil.
O teste
Adicionamos um teste ao firmware que escreve o buffer de voo da mesma forma que o logger de voo faz enquanto o loop de 500Hz continua funcionando, então relata o quanto o loop foi prejudicado. Ele escreve a 55kB/s, depois 166kB/s, depois em plena velocidade, depois apaga tudo o que escreveu. Essas duas taxas foram estabelecidas a partir de nosso plano anterior de float (18 bytes por amostra) para logging de 2kHz e 8kHz, portanto são um pouco acima do que o formato inteiro precisa, o que torna o teste um pouco mais difícil que a vida real. Para deixar claro uma coisa, o IMU nesta placa ainda está em I2C a 2kHz, portanto as fases mais rápidas são a carga de flash de logging mais rápido, não amostragem mais rápida. Mesma placa, mesmo teste, apenas as bibliotecas Arduino trocadas.
Cada célula é o núcleo Arduino padrão primeiro, depois nossas bibliotecas reconstruídas com o programa no PSRAM e ECC ativado. O loop deveria funcionar a 500Hz, uma passagem a cada 2ms, e o pior intervalo é o tempo mais longo entre duas passagens.
| Enquanto a flash está | Taxa de loop | Pior intervalo | Transbordos de FIFO |
|---|---|---|---|
| Escrevendo a 55kB/s | 349 → 499Hz | 15.8 → 5.4ms | 42 → 0 |
| Escrevendo a 166kB/s | 193 → 499Hz | 15.0 → 5.2ms | 77 → 0 |
| Escrevendo em plena velocidade | 196 → 499Hz | 12.9 → 4.6ms | 24 → 0 |
| Apagando (blocos de 64KB) | 17 → 499Hz | 202 → 4.9ms | 3 → 0 |
No núcleo padrão, o loop cai aos pedaços assim que a flash fica ocupada, ele nem conseguia acompanhar a fase de 166kB/s (conseguiu 160), e o apagamento o para por 200ms. Cada transbordamento é o FIFO do IMU enchendo e amostras sendo perdidas. Com o programa no PSRAM, mantém 499Hz através de tudo isso, incluindo o apagamento. O pior intervalo de cerca de 5ms é apenas a passagem mais lenta do loop (ele faz isso sem nenhuma escrita de flash acontecendo), o FIFO do IMU absorve, e nada é perdido.
Codificação delta
Codificação delta significa armazenar o quanto cada leitura mudou desde a anterior em vez da própria leitura. Nessas taxas, uma amostra é muito próxima da próxima, portanto a maioria das mudanças cabe em um ou dois bytes em vez de dois ou três. É aqui que os inteiros ganham seu valor: a mudança entre dois inteiros é ela mesma um pequeno inteiro, enquanto os bits de um float não diminuem quando o valor mal se move. Em passos de 1/2048 g, uma mudança de até cerca de 4g entre amostras cabe em 2 bytes.
O quanto economiza depende do que o foguete está fazendo. Sentado na plataforma ou em voo planado, a maioria das amostras cairia para 6 a 8 bytes, mas no boost com o motor fazendo tudo vibrar é mais como 11, e o boost é o que importa pois é onde a taxa de logging está em seu pico. Essa é nossa estimativa aproximada em vez de uma medição por enquanto. O Jupiter não codifica em delta o buffer de voo no momento, pois não precisa para velocidade. Onde ajudaria é no tempo de gravação: o buffer de voo de 7.8MB tem cerca de 58 segundos a 8kHz completo bruto, ou cerca de 75 segundos codificados em delta, antes do logging híbrido abaixar a taxa.
Então quão rápido poderia registrar?
Juntando as velocidades de flash medidas com os tamanhos de dados acima fornece o máximo que cada configuração consegue manter:
| Configuração | Flash, em plena velocidade (kB/s) | Taxa máxima, bruto / delta (kHz) | Dados de 8kHz, bruto / delta (kB/s e parcela de flash) |
|---|---|---|---|
| Núcleo padrão, buffer no PSRAM* | 160 | 9 / 12 | 140 / 108 88% / 68% |
| Núcleo padrão, buffer na RAM interna* | 306 | 19 / 25 | 140 / 108 46% / 35% |
| Bibliotecas reconstruídas, buffer no PSRAM | 209 | 12 / 17 | 140 / 108 67% / 52% |
| Bibliotecas reconstruídas, buffer na RAM interna (o Jupiter) | 335 | 21 / 28 | 140 / 108 42% / 32% |
* A flash acompanha no núcleo padrão mas o loop não. Caiu abaixo de 200Hz e perdeu amostras do IMU em cada taxa que tentamos, portanto na prática não consegue registrar limpo a nenhuma dessas.
Bruto é 15 bytes por amostra, codificado em delta é nossa estimativa de boost de 11 bytes, ambos incluem os 20kB/s de tudo mais, e são as velocidades em plena velocidade medidas com o loop funcionando, arredondadas para baixo sem nada guardado de sobra. O barramento SPI de 10MHz também não é o limite em nenhuma dessas, poderia carregar em torno de 59.000 amostras por segundo.
Portanto 8kHz precisa de cerca de 140kB/s contra os 335kB/s que a flash suporta em plena velocidade, cerca de 42% sem nenhuma codificação delta, ou cerca de 108kB/s (32%) com ela. A CPU também não está trabalhando muito: a 2kHz a maior parte de cada passagem do loop é gasta esperando no barramento I2C, e no SPI de 10MHz a mesma leitura a 8kHz deveria levar cerca de 0.3ms. O próprio IMU limita-se a 8kHz de qualquer forma, portanto o resto é margem. Ainda é teste de bancada até a placa SPI chegar, e os padrões serão mais baixos de qualquer forma, com o logging abaixando sua taxa após o boost, mas o S3 parece bem preparado para isso.
Experimente você mesmo
Já que fizemos o trabalho pesado, as bibliotecas ESP32-S3 reconstruídas estão nos downloads no final desta postagem para qualquer um que esteja usando Arduino. Elas são para o esp32 core 3.3.12 do Boards Manager apenas, e substituem esta pasta:
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
No Windows, feche a IDE Arduino, depois abra um Prompt de Comando (digite cmd no menu Iniciar, não PowerShell) e cole isto. Encontra o zip na sua pasta Downloads qualquer que seja o que o download adicionou ao seu nome, e mantém sua pasta 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"
A última linha limpa a cópia em cache da IDE do núcleo compilado para que se recompile contra as novas bibliotecas. Para voltar ao padrão:
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"
Não use Extrair Tudo do Explorer no zip, alguns dos caminhos dentro são muito longos e será silenciosamente ignorado arquivos. O comando acima ou 7-Zip são bons.
Algumas coisas a saber:
- Testamos em um S3 com 8MB de PSRAM octal (o S3R8), com PSRAM definido para OPI PSRAM no menu Ferramentas. Não testamos em PSRAM quad, e como o programa é executado a partir do PSRAM não servirá para placas sem nenhum.
- O programa é copiado no PSRAM na inicialização, portanto você perde essa quantidade de PSRAM, mais um décimo sexto disso para ECC.
- Foi compilado com o esp32-arduino-lib-builder da Espressif no ESP-IDF 5.5.5, o mesmo das bibliotecas oficiais 3.3.12, com apenas duas configurações adicionadas. Todo o crédito vai para Espressif. É não oficial e como está, portanto mantenha sua pasta stock.
Se preferir compilá-lo você mesmo, é o esp32-arduino-lib-builder da Espressif (Linux ou WSL), verificado nas versões listadas no arquivo versions.txt na pasta de bibliotecas oficiais 3.3.12, com estas duas linhas adicionadas a configs/defconfig.esp32s3 antes de executar a compilação:
CONFIG_SPIRAM_XIP_FROM_PSRAM=y CONFIG_SPIRAM_ECC_ENABLE=y
O IMU do Jupiter está migrando para SPI de 10MHz na próxima placa para que possa registrar a 8kHz, acima dos 2kHz que consegue em I2C atualmente. No Rocketry Forum alguém levantou uma questão legítima sobre se um ESP32-S3 a 240MHz realmente consegue "registrar" a 8kHz, em vez de apenas ler o sensor nessa velocidade. É uma pergunta válida e, embora eu estivesse confiante de que conseguíríamos fazer isso, ainda não tinha verificado, então em vez de adivinhar fizemos uma medição apropriada. Isto é o que descobrimos, e no final há a biblioteca Arduino reconstruída que acabamos por usar, se quiser experimentar em seus próprios projetos.

