<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>blog.tedder.dev (Posts about esp-idf)</title><link>https://blog.tedder.dev/</link><description></description><atom:link href="https://blog.tedder.dev/categories/esp-idf.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2026 &lt;a href="mailto:ted@tedder.me"&gt;tedder&lt;/a&gt; </copyright><lastBuildDate>Sun, 07 Jun 2026 20:21:52 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>ESP-IDF project standards and conventions</title><link>https://blog.tedder.dev/posts/esp-idf-project-standards/?utm_source=/categories/esp-idf.xml&amp;utm_medium=nikola_feed&amp;utm_campaign=rss_feed</link><dc:creator>tedder</dc:creator><description>&lt;p&gt;I've built a bunch of ESP32 projects using Espressif's ESP-IDF framework, and every time I start a new one, I second-guess myself on basic structure: where does the config go? How do I version the firmware? What partition layout should I use? This post is documentation-to-self, but hopefully it saves someone else from the iterative debugging I've done.&lt;/p&gt;
&lt;h3&gt;Project structure and build configuration&lt;/h3&gt;
&lt;p&gt;The ESP-IDF follows a pretty strict layout. At the top level you'll have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;main/&lt;/code&gt;: your application code (&lt;code&gt;main.c&lt;/code&gt; is the entry point)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CMakeLists.txt&lt;/code&gt;: project-level build configuration&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sdkconfig.defaults&lt;/code&gt;: sensible defaults for menuconfig options&lt;/li&gt;
&lt;li&gt;&lt;code&gt;partitions.csv&lt;/code&gt;: partition table (more on that below)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.clangd&lt;/code&gt;: language server config to fix false positives on macOS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;main/CMakeLists.txt&lt;/code&gt; is minimal:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;idf_component_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SRCS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"main.c"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;                       &lt;/span&gt;&lt;span class="n"&gt;INCLUDE_DIRS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Keep components in &lt;code&gt;main/&lt;/code&gt; until you have more than a few files, then break them into &lt;code&gt;components/mycomponent/&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;OTA update patterns: ota_0 and ota_1&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Never use the factory+ota_0 partition layout.&lt;/strong&gt; I learned this the hard way when I bricked a device. Use &lt;strong&gt;ota_0 and ota_1 instead&lt;/strong&gt;, so you can always fall back to the previous firmware if an update goes wrong.&lt;/p&gt;
&lt;p&gt;Your &lt;code&gt;partitions.csv&lt;/code&gt; should look like:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;nvs,      data, nvs,     0x9000, 0x6000,
otadata,  data, ota,     0xf000, 0x2000,
ota_0,    app,  ota_0,   0x20000, 0x1C0000,
ota_1,    app,  ota_1,   0x1E0000, 0x1C0000,
spiffs,   data, spiffs,  0x3A0000, 0x60000,
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The sizes here depend on your flash size (I'm assuming 4MB). The key is that &lt;code&gt;otadata&lt;/code&gt; tracks which OTA partition is active, and the bootloader automatically switches between them. On a bad update, it rolls back to the previous partition on next boot.&lt;/p&gt;
&lt;p&gt;I allocate 1.75 MB (0x1C0000) per OTA partition because my firmware is usually 600-800 KB, and that leaves plenty of headroom for growth without requiring a reflash to resize.&lt;/p&gt;
&lt;h3&gt;Firmware versioning&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Every binary that gets flashed or pushed to S3 needs a version bump.&lt;/strong&gt; Use a simple semver string:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;// main/main.c&lt;/span&gt;
&lt;span class="cp"&gt;#define FIRMWARE_VERSION "1.2.3"&lt;/span&gt;
&lt;span class="cp"&gt;#define FIRMWARE_VERSION_MAJOR 1&lt;/span&gt;
&lt;span class="cp"&gt;#define FIRMWARE_VERSION_MINOR 2&lt;/span&gt;
&lt;span class="cp"&gt;#define FIRMWARE_VERSION_PATCH 3&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then in your OTA or setup code, log it:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;ESP_LOGI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Firmware version: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FIRMWARE_VERSION&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This sounds obvious, but I've spent frustrating hours trying to reproduce a bug and realizing the device is running an older build than I thought. A simple version string at startup saves that pain.&lt;/p&gt;
&lt;p&gt;If you're pushing to S3 for OTA (see below), include the version in the build artifact name: &lt;code&gt;myproject-v1.2.3.bin&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;WiFi authentication: stick with WPA2_PSK&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Always use &lt;code&gt;WIFI_AUTH_WPA2_PSK&lt;/code&gt;, not &lt;code&gt;WIFI_AUTH_WPA2_WPA3_PSK&lt;/code&gt;.&lt;/strong&gt; The mixed mode causes mysterious auth failures on home routers, especially older ones or devices that don't advertise both protocols equally.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;wifi_config_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;wifi_config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ssid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;CONFIG_WIFI_SSID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;CONFIG_WIFI_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authmode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;WIFI_AUTH_WPA2_PSK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Even if your home network supports WPA3, the mixed mode is a footgun. If you're deploying to multiple locations or devices with varying WiFi hardware, stick with WPA2_PSK. It's universally supported and won't surprise you.&lt;/p&gt;
&lt;h3&gt;Partition sizing and resizing pain&lt;/h3&gt;
&lt;p&gt;Leave expansion room in your OTA partitions. I allocate 1.75 MB when my current firmware is 600 KB because resizing partitions later requires a USB cable and &lt;code&gt;esptool.py flash_id&lt;/code&gt; followed by a full reflash. It's doable but annoying.&lt;/p&gt;
&lt;p&gt;If your project grows and you need more space, you have two options:
1. Resize partitions (USB reflash required)
2. Move to a larger flash chip (harder)&lt;/p&gt;
&lt;p&gt;Allocate generously at the start. A few MB of unused space costs nothing.&lt;/p&gt;
&lt;h3&gt;Clangd/LSP false positives on macOS&lt;/h3&gt;
&lt;p&gt;If you're using clangd or VSCode's C/C++ extension on macOS, you'll see tons of red squiggles for ESP-IDF types and macros that actually compile fine. This is because clangd doesn't know about the build configuration.&lt;/p&gt;
&lt;p&gt;Create a &lt;code&gt;.clangd&lt;/code&gt; file at your project root:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nt"&gt;CompileFlags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;CompilationDatabase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;build/compile_commands.json&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Build once (so &lt;code&gt;compile_commands.json&lt;/code&gt; exists), then clangd will use it and the false positives disappear. This is especially important for understanding macro expansions and avoiding frustration while editing.&lt;/p&gt;
&lt;h3&gt;S3 OTA uploads: the cache-control trap&lt;/h3&gt;
&lt;p&gt;If you're pushing built binaries to S3 for OTA updates, &lt;strong&gt;always include &lt;code&gt;--cache-control "max-age=30"&lt;/code&gt;&lt;/strong&gt; when uploading:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nv"&gt;AWS_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yourprofile&lt;span class="w"&gt; &lt;/span&gt;aws&lt;span class="w"&gt; &lt;/span&gt;s3&lt;span class="w"&gt; &lt;/span&gt;cp&lt;span class="w"&gt; &lt;/span&gt;build/myproject.bin&lt;span class="w"&gt; &lt;/span&gt;s3://your-bucket/firmware/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--cache-control&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"max-age=30"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Without this, CloudFront (or any other CDN in front of S3) will serve stale versions for up to 1 hour, and your devices will pull the old firmware. I've spent hours debugging "why is the new firmware not deployed?" only to realize the CDN was serving the previous build.&lt;/p&gt;
&lt;p&gt;Set &lt;code&gt;max-age&lt;/code&gt; to something short (30 seconds works) so you get fresh binaries on each request, not "fresh every hour."&lt;/p&gt;
&lt;h3&gt;Battery-powered sensors: ESP-NOW vs WiFi&lt;/h3&gt;
&lt;p&gt;If you're building a battery-powered sensor, &lt;strong&gt;ESP-NOW uses far less power than WiFi&lt;/strong&gt;, but requires a receiver that's always on (or close to it). Tradeoffs:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WiFi:&lt;/strong&gt;
- Higher latency and power draw (radio startup is expensive)
- Works everywhere without custom infrastructure
- Built-in over-the-air update support&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ESP-NOW:&lt;/strong&gt;
- One-way or paired communication only; you need a gateway/receiver
- Extremely low power if you can wake, transmit, and sleep in milliseconds
- Mesh capability with enough peers&lt;/p&gt;
&lt;p&gt;For a humidity sensor that transmits once per hour, ESP-NOW + a gateway is overkill. For a door sensor that needs to wake and transmit immediately, ESP-NOW is worth the complexity. For anything in between, build WiFi first and optimize to ESP-NOW later if battery life becomes critical.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;That's the foundation. Every new project I start, I copy the partition table, firmware version pattern, and build wrapper from the last one, and I've saved myself countless hours of "wait, why is this partition 512K?" debugging. Hopefully this saves you from the same mistakes.&lt;/p&gt;</description><category>embedded</category><category>esp32</category><category>esp-idf</category><category>firmware</category><guid>https://blog.tedder.dev/posts/esp-idf-project-standards/</guid><pubDate>Sun, 07 Jun 2026 17:23:00 GMT</pubDate></item></channel></rss>