Go back

ESP32 Development Notes: Async HTTP, GIF Playback, and Resource OTA

Published:  at  06:45 AM
⏱️ 1163 words • 6 min read

阅读中文版

Four ESP32 development notes: asynchronous HTTP, keeping PWM active during light sleep, GIF playback, and OTA updates for SPIFFS resources.

Preface

Many times, when we build an ESP32 project from scratch, we often fall into various “pits”.

In some previous projects (such as Make a desktop with a small pixel screen), I encountered various problems: network requests stuck in the main thread, the screen display was too monotonous, PWM stalled during sleep, and the pain of reflashing the entire firmware when only updating a few pictures at a time.

After smoothing these pitfalls, I compiled four very practical tips in ESP32 development. In order to avoid everyone making the same mistakes, and to facilitate my own future reference (copy), this article will summarize these technical points.

I hope it can provide some inspiration to you who are struggling with ESP32.


Tip 1: Refuse to block! Use asynchronous network requests

Pain point analysis

In the early pixel small screen project, in order to obtain the number of fans of station B, I directly used the synchronous HTTP Client. The result is that the entire device’s UI will be stuck for hundreds of milliseconds or even seconds every time a network request is made. This is a disaster for the user experience - you can’t make the user feel like the device is “dead”.

For elegance, asynchronous must be used.

solution

We can utilize the AsyncTCP library to implement non-blocking HTTP requests. Although the callback function (Callback) is a bit too much to write, the result is a silky-smooth main loop.

core code

The AsyncTCP-esphome library is used here.

#include <Arduino.h>
#include <AsyncTCP.h>
#include <WiFi.h>

// ... WiFi配置省略 ...

void asyncReqeust() {
static AsyncClient \*aClient;
if (aClient) return; // 防止重复创建

aClient = new AsyncClient();
if (!aClient) return;

// 注册错误回调
aClient->onError([](void *arg, AsyncClient *client, int error) {
Serial.println("Connect Error");
aClient = NULL;
delete client;
}, NULL);

// 注册连接回调
aClient->onConnect([](void *arg, AsyncClient *client) {
Serial.println("Connected");
aClient->onError(NULL, NULL);

      // 注册断开连接回调
      client->onDisconnect([](void *arg, AsyncClient *c) {
          aClient = NULL;
          delete c;
          Serial.println("Disconnected");
      }, NULL);

      // 注册数据接收回调(核心逻辑)
      client->onData([](void *arg, AsyncClient *c, void *data, size_t len) {
          Serial.write((uint8_t *)data, len);
          // 这里可以解析JSON数据
      }, NULL);

      // 发送HTTP GET请求
      client->write("GET /x/relation/stat?vmid=14374079 HTTP/1.1\r\n"
                    "Host: api.bilibili.com\r\n"
                    "Content-Type: application/json; charset=utf-8\r\n\r\n");

}, NULL);

if (!aClient->connect("api.bilibili.com", 80)) {
Serial.println("Connect Fail");
AsyncClient \*client = aClient;
aClient = NULL;
delete client;
}
}

In this way, network requests are performed silently in the background, and your main loop loop() can still run quickly to handle key scanning or screen refresh.


Tip 2: Make the picture move - play GIF animations

Visual upgrade

The network is smooth and the interface cannot be too shabby. When making a device with a 240x135 resolution, I really couldn’t think of any advanced algorithm animation, so I decided to be “lazy”: play GIF emoticons directly on the screen.

It is recommended to use the AnimatedGIF library, combined with the TFT_eSPI driver, and the effect is very good.

Make GIF header file

First, we need to convert the GIF file into an array that the code can read. Under Linux or WSL subsystem, one line of xxd command can do it:

## 将GIF转换为C数组
xxd -i angry_80px.gif >> loading.h

The generated loading.h contains a huge unsigned char array.

driver code

The code is modified based on the official example and adapted to TFT_eSPI.

##include <AnimatedGIF.h>
##include <TFT_eSPI.h>
##include "loading.h" // 刚才生成的头文件

AnimatedGIF gif;
TFT_eSPI tft = TFT_eSPI();

// 回调函数:将解码后的一行像素推送到屏幕
void GIFDraw(GIFDRAW \*pDraw) {
// ... 核心绘制逻辑,包含透明度处理和DMA加速 ...
// 篇幅原因,核心逻辑是调用 tft.pushPixels 将 pDraw->pPixels 推送显示
// 完整逻辑参考 AnimatedGIF 的 TFT_eSPI_memory 示例
}

void setup() {
tft.begin();
tft.setRotation(1);
tft.fillScreen(TFT_BLACK);
gif.begin(BIG_ENDIAN_PIXELS);
}

void loop() {
if (gif.open((uint8_t \*)angry_80px_gif, sizeof(angry_80px_gif), GIFDraw)) {
while (gif.playFrame(true, NULL)) {
yield(); // 喂狗,防止复位
}
gif.close();
}
}

As long as the memory is large enough, it is not impossible to put a GIF of Cai Xukun playing basketball (escape).


Tip 3: Maintain PWM output in Light-Sleep mode

strange needs

In some scenarios (such as backlight maintenance), we need ESP32 to enter Light-Sleep to save power, but we do not want the PWM signal of the screen backlight to be interrupted. By default, the high-speed clock is turned off after entering sleep, causing PWM to stall.

Turn on RTC8M clock

After consulting the ESP-IDF manual, I found that if the PWM clock source is configured as RTC8M_CLK, it can work even under Light-Sleep. But there is a premise, and menuconfig needs to be modified.

  1. Environment configuration:

    idf.py menuconfig

    Enter Component config -> Hardware Settings -> Sleep Config. Be sure to close light sleep GPIO reset workaround.

  2. Code implementation: Configure LEDC using ESP-IDF native API.

#include "driver/ledc.h"
#include "esp_sleep.h"

void app_main(void) {
// 1. 定时器配置:重点是选用 LEDC_USE_RTC8M_CLK
ledc_timer_config_t ledc_timer = {
.duty_resolution = LEDC_TIMER_13_BIT,
.freq_hz = 1000,
.speed_mode = LEDC_LOW_SPEED_MODE, // 必须是低速模式
.timer_num = LEDC_TIMER_0,
.clk_cfg = LEDC_USE_RTC8M_CLK, // 关键!
};
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));

// 2. 通道配置... (常规配置,略)

// 3. 强制开启RTC8M电源域
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC8M, ESP_PD_OPTION_ON);

while (1) {
// 设置一个占空比
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 1000);
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);

    // 进入浅睡眠,PWM依然会保持输出
    esp_sleep_enable_timer_wakeup(1000 * 1000 * 5); // 睡5秒
    esp_light_sleep_start();

}
}

This function has been measured to be very useful for reducing power consumption.


Tip 4: Only update resource files? Try SPIFFS Partition OTA

scene

As the project gets bigger, I find a problem: sometimes I just want to update the image resources or font library in the UI, but don’t want to update the code. Traditional OTA updates the App partition, which wastes traffic and time. In fact, we can perform OTA only for the SPIFFS data partition.

Partition table design

First, we need to customize the partition table (partitions.csv) to separate the data.

## Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x5000,
otadata,  data, ota,     0xe000,  0x2000,
app0,     app,  ota_0,   0x10000, 0x300000,
storage,  data, spiffs,  0x310000, 0xC000,

Here I have allocated a 48KB storage partition for demonstration.

OTA core logic

Unlike updating the app, updating the partition is actually a “erasure + write” process. Assume that the new file image has been downloaded into memory over the network (or, as in this example, embed directly in the code for the sake of demonstration).

void update_spiffs_partition() {
// 1. 查找 SPIFFS 分区
const esp_partition_t \*spiffs_part = esp_partition_find_first(
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, NULL);

    if (spiffs_part == NULL) {
        ESP_LOGE("OTA", "SPIFFS partition not found!");
        return;
    }

    // 2. 擦除原分区内容
    ESP_LOGI("OTA", "Erasing partition...");
    esp_partition_erase_range(spiffs_part, 0, spiffs_part->size);

    // 3. 写入新数据
    // 假设 spiffs2_start 和 spiffs2_end 是新镜像在内存中的地址
    size_t image_size = spiffs2_end - spiffs2_start - 1;
    ESP_LOGI("OTA", "Writing new data: %d bytes", image_size);

    esp_partition_write(spiffs_part, 0, spiffs2_start, image_size);

    ESP_LOGI("OTA", "Done! Restarting...");
    esp_restart();

}

This technique is especially useful when doing functions such as image replacement and font switching. It does not require touching the core code, and is safe and fast.


Summary

The above are the four practical tips I summarized when playing with ESP32 recently. From asynchronous requests to avoid blocking, to fancy GIF playback, to PWM control under low power consumption and flexible resource OTA, every point is explored in actual development to solve specific pain points.

This is probably the joy of hardware development: encountering a pit, filling it, and then watching the device run as expected. That sense of accomplishment is irreplaceable.

Hope these notes are helpful and see you on the next project!


Share this post on:

Previous Post
Mapping and Modifying the Lenovo R720 Keyboard Matrix
Next Post
ESP32 LCD Performance: Parallel Interface versus SPI