前言
很多时候,我们从零开始构建一个ESP32项目,往往会掉进各种各样的“坑”里。
在之前的一些项目中(比如做个桌面像素小屏幕吧),我遇到过各种各样的问题:网络请求卡死主线程、屏幕显示太单调、休眠时PWM停转、以及每次只更新几张图片却要重刷整个固件的痛苦。
把这些坑踩平之后,我整理了四个在ESP32开发中非常实用的技巧。为了避免大家重蹈覆辙,也为了方便我自己日后查阅(Copy),这篇文章将把这些技术点汇总起来。
希望能给正在折腾ESP32的你提供一些灵感。
技巧一:拒绝阻塞!使用异步网络请求
痛点分析
在早期的像素小屏幕项目中,我为了获取B站粉丝数,直接使用了同步的HTTP Client。结果就是每次请求网络时,整个设备的UI都会卡住几百毫秒甚至几秒。这对于用户体验来说简直是灾难——你不能让用户觉得设备“死机”了。
为了优雅,必须上异步。
解决方案
我们可以利用 AsyncTCP 库来实现非阻塞的HTTP请求。虽然写起来回调函数(Callback)套娃有点多,但换来的是丝般顺滑的主循环。
核心代码
这里使用的是 AsyncTCP-esphome 库。
AsyncRequest.cpp1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| #include <Arduino.h> #include <AsyncTCP.h> #include <WiFi.h>
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); }, NULL);
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; } }
|
这样,网络请求在后台默默进行,你的主循环 loop() 依然可以跑得飞起,去处理按键扫描或者屏幕刷新。
技巧二:让画面动起来——播放GIF动图
视觉升级
网络通畅了,界面也不能太寒酸。在做一个带有240x135分辨率的装置时,我实在想不出什么高级的算法动画,于是决定“偷懒”:直接在屏幕上播放GIF表情包。
这里推荐使用 AnimatedGIF 库,配合 TFT_eSPI 驱动,效果非常不错。
制作GIF头文件
首先,我们需要把GIF文件转换成代码能读取的数组。在Linux或者WSL子系统下,一行 xxd 命令就能搞定:
1 2
| xxd -i angry_80px.gif >> loading.h
|
生成的 loading.h 里面就是一个巨大的 unsigned char 数组。
驱动代码
代码基于官方示例修改,适配了 TFT_eSPI。
GifPlayer.cpp1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include <AnimatedGIF.h> #include <TFT_eSPI.h> #include "loading.h"
AnimatedGIF gif; TFT_eSPI tft = TFT_eSPI();
void GIFDraw(GIFDRAW *pDraw) { }
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(); } }
|
只要内存够大,放个蔡徐坤打篮球的GIF也不是不可能(逃)。
技巧三:Light-Sleep模式下保持PWM输出
奇怪的需求
有些场景下(比如背光保持),我们需要ESP32进入 Light-Sleep 省电,但又不希望 屏幕背光的PWM 信号中断。
默认情况下,进入睡眠后高速时钟会关闭,导致 PWM 停摆。
开启 RTC8M 时钟
查阅 ESP-IDF 手册发现,如果将 PWM 的时钟源配置为 RTC8M_CLK,即使在 Light-Sleep 下也能工作。但这有个前提,需要修改 menuconfig。
环境配置:
进入 Component config -> Hardware Settings -> Sleep Config。
务必关闭 light sleep GPIO reset workaround。
代码实现:
使用 ESP-IDF 原生 API 配置 LEDC。
pwm_sleep.c1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include "driver/ledc.h" #include "esp_sleep.h"
void app_main(void) { 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));
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); esp_sleep_enable_timer_wakeup(1000 * 1000 * 5); esp_light_sleep_start(); } }
|
实测这个功能对于降低功耗非常有用。
技巧四:只更新资源文件?试试 SPIFFS 分区 OTA
场景
随着项目越来越大,我发现一个问题:有时候我只想更新一下UI里的图片资源或者字体库,并不想更新代码。
传统的OTA是更新 App 分区,这很浪费流量和时间。其实我们完全可以只针对 SPIFFS 数据分区进行 OTA。
分区表设计
首先,我们需要自定义分区表(partitions.csv),把数据独立出来。
1 2 3 4 5
| # 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,
|
这里我划了一个 48KB 的 storage 分区用于演示。
OTA 核心逻辑
不同于更新 App,更新分区实际上就是“擦除 + 写入”的过程。假设新的文件镜像已经通过网络下载到了内存中(或者像本例一样,为了演示直接 embed 在代码里)。
spiffs_ota.c1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| void update_spiffs_partition() { 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; }
ESP_LOGI("OTA", "Erasing partition..."); esp_partition_erase_range(spiffs_part, 0, spiffs_part->size);
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(); }
|
这个技巧在做图片更换、字体切换等功能时特别好用,不用动核心代码,安全又快速。
总结
以上就是我近期折腾ESP32时总结的四个实用技巧。
从避免阻塞的异步请求,到花里胡哨的GIF播放,再到低功耗下的PWM控制和灵活的资源OTA,每一个点都是在实际开发中为了解决特定痛点而摸索出来的。
硬件开发的乐趣大概就在于此:遇到一个坑,填平它,然后看着设备按照预想的方式运行,那种成就感是无法替代的。
希望这些笔记对你有所帮助,我们下个项目见!