Bad Apple but on ESP32-S3 & SSD1306 OLED
I just got my first little SSD1306 OLED screen and wanted to do something cool with it after running some tests. So as any sane person would do, I decided to show Bad Apple on it; just to remember I don’t have enough brain cells left to pull it off by myself. So I searched the web and found this great project: hackffm/ESP32_BadApple. They deserve most of the credit, since everything I did was based on their excellent work.
The repo contains compressed video data, and an Arduino sketch along with a few C++ files to decompress and display the actual video frames, which are stored on the microcontroller’s flash as a separate filesystem called SPIFFS, along with the code that we upload to the microcontroller. The problem is that SPIFFS data need to be uploaded manually, as Arduino IDE itself is not quite aware of it.
Fortunately, the repo includes instructions for that, which points us to an Arduino plugin—but that plugin no longer works with the newer 2.x.x versions of the IDE. Still, nothing stops us from reading the plugins source code and replicating it’s functionality ourselves.
Getting our feet wet
A skim through ESP32FS.java reveals that we just need to figure out three steps:
-
Read the partition table by opening the selected partition table’s csv file and looking for the spiffs entry to figure out it’s size and offset into the flash.
- Create an SPIFFS image.
This is pretty straight forward: Just invoke
mkspiffs
on the sketch’sdata/
directory as we see in the plugin code:if (listenOnProcess(new String[] { toolPath, // Path to the mkspiffs binary "-c", dataPath, // Path to the data directory "-p", spiPage+"", // spiPage = 256 (constant) "-b", spiBlock+"", // spiBlock = 4096 (constant) "-s", spiSize+"", // Read from partition table imagePath }) != 0) // ...
- Writing the image to ESP32’s flash
with
esptool.py
(or send it over the network withespota.py
, in which case we don’t need to explicitly specify the partition address):sysExec(new String[] { // Path to esptool.py (or a binary executable variant) esptool.getAbsolutePath(), "--chip", mcu, // Chip variant (in this case, 'esp32s3'), read from Arduino's configuration "--baud", uploadSpeed, // The upload speed to use, read from Arduino's configuration // The serial port that is connected to the micro, read from Arduino // (e.g. /dev/ttyACM0, /dev/ttyUSB0, or something else on other OSes) "--port", serialPort, "--before", "default_reset", "--after", "hard_reset", "write_flash", "-z", // Compress data in transfer // The flash mode to use (and set), read from Arduino's configuration // (See https://docs.espressif.com/projects/esptool/en/latest/esp32/advanced-topics/spi-flash-modes.html) "--flash_mode", flashMode, // The flash frequency to use (and set), read from Arduino's configuration // (See https://docs.espressif.com/projects/esptool/en/latest/esp32s3/esptool/flash-modes.html#flash-frequency-flash-freq-ff) "--flash_freq", flashFreq, // The flash size to set // (See https://docs.espressif.com/projects/esptool/en/latest/esp32s3/esptool/flash-modes.html#flash-size-flash-size-fs) "--flash_size", "detect", // The address to begin writing the image to (read from the partition table) ""+spiStart, imagePath });
Before panicking: many of these flags are not required.
-z
is enabled by default, flash options can be ommited to use device defaults, chip type can be detected automatically, and--before
and--after
flags are not quite necessary as the chip will be hard-reset afterwrite_flash
anyway.
Reading the partition table
The partition table is set by the Arduino IDE when writing the firmware on the flash. So to know what partition table is currently being written, There are a couple of options:
- See what Arduino is configured to use by looking in the
Tools
menu:then search Arduino package files for the partition table’s csv file. They are located somewhere like
~/.arduino15/packages/esp32/hardware/esp32/x.y.z/tools/partitions/
:$ ls ~/.arduino15/packages/esp32/hardware/esp32/3.2.0/tools/partitions/ app3M_fat9M_16MB.csv esp_sr_16.csv minimal.csv tinyuf2-partitions-16MB-noota.csv app3M_fat9M_fact512k_16MB.csv ffat.csv min_spiffs.csv tinyuf2-partitions-4MB.csv app3M_spiffs9M_fact512k_16MB.csv huge_app.csv no_fs.csv tinyuf2-partitions-4MB-noota.csv bare_minimum_2MB.csv large_fat_32MB.csv noota_3g.csv tinyuf2-partitions-8MB.csv boot_app0.bin large_ffat_8MB.csv noota_3gffat.csv tinyuf2-partitions-8MB-noota.csv default_16MB.csv large_littlefs_32MB.csv no_ota.csv zigbee_2MB.csv default_32MB.csv large_spiffs_16MB.csv noota_ffat.csv zigbee_8MB.csv default_8MB.csv large_spiffs_8MB.csv ota_nofs_4MB.csv zigbee.csv default.bin m5stack_partitions_16MB_factory_4_apps.csv rainmaker_4MB_no_ota.csv zigbee_zczr_2MB.csv default.csv m5stack_partitions_16MB_factory_6_apps.csv rainmaker_8MB.csv zigbee_zczr_8MB.csv default_ffat_8MB.csv max_app_4MB.csv rainmaker.csv zigbee_zczr.csv default_ffat.csv max_app_8MB.csv tinyuf2-partitions-16MB.csv
In this case, it is the
default.csv
. However if you are unsure, you can use the second method below. -
or Read the partition table directly from the flash. There is an offset into the flash where the partition table is written in binary which varies between different configurations, but the default offset is
0x8000
and the table size is0xC00
bytes.Knowing that, we can continue by dumping that section into a file:
esptool.py --port /dev/ttyACM0 read_flash 0x8000 0xc00 partition-table.bin
Then, we parse the binary to csv:
gen_esp32part.py partition-table.bin
(The
gen_esp32part.py
script is included with ESP-IDF and Arduino’s ESP32 package. You can try searching for it in the Arduino package files if you have trouble finding it)
Anyhow, you should now have a csv file which looks like this:
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x140000,
app1, app, ota_1, 0x150000,0x140000,
spiffs, data, spiffs, 0x290000,0x160000,
coredump, data, coredump,0x3F0000,0x10000,
Just note down the Offset
and Size
fields of the spiffs
partition.
Depending in the method you used to get this csv table, these numbers might
be in hex, decimal and may or may not have postfixes. Remember to assume
binary prefixes (e.g 1024
instead of 1000) for the postfixes. Also, all these values are in bytes.
Creating the SPIFFS image
Easy, just run this and make sure to replace the number after -s
with
whatever SPIFFS partition size you had:
mkspiffs -c data/ -p 256 -b 4096 -s 1441792 spiffs.bin
(Again, mkspiffs
is included in Arduino package files)
Write the image to ESP32’s flash
At this point, this is also pretty easy to do (Don’t froget to replace 2686976
with your SPIFFS partition offset):
esptool.py \
--chip esp32s3 \
--baud 921600 \
--port /dev/ttyACM0 \
--before default_reset \
--after hard_reset \
write_flash \
-z \
--flash_mode qio \
--flash_freq 80m \
--flash_size detect \
2686976 ./spiffs.bin
All these values can be gathered from the same Tools
menu mentioned above.
But remember, this command can be simplified since alot of these flags are
not mandatory:
esptool.py --baud 921600 --port /dev/ttyACM0 write_flash 2686976 ./spiffs.bin
(I kept --baud
because the default rate was too slow. If you experience any weired
behavior, consider removing this flag or using lower baud rates.)
Final thoughts
Although this method works perfectly fine for this example, there are many other options when it comes to mounting a filesystem on the ESP32, such as FatFS or LittleFS which can both operate on the SPI flash as well as on an SD card. They also perform differently. So you might want to choose a different one depending on your needs.
Epilogueealisdjaewr?
Finally, it’s time to enjoy watching Bad Apple on the OLED screen with a cup of warm coffee ☕.
The only thing it lacks, is the audio was added on top of the video with editing software. I really hope some day I comeback to this type of stuff and maybe make a version with audio. Until then!