ESP-IDF 工程能力 / 任务与队列发布 内容更新

FreeRTOS 多任务第一课:采样、显示和网络通过队列协作

用SHT40、SSD1306与局域网HTTP做生产者和两个消费者,故意拖慢消费并明确覆盖/丢新策略;保留单核双核构建与实板栈、延迟记录方法。

读完能做什么
得到有明确有界队列策略的采样/显示/网络工程,能主动制造慢消费者并按统一方法验收掉样、栈和单双核差异。
开始之前
先完成所列前置,再按本例固定板型与README执行;本文不把主机检查当成实体运行。
所需器材
指定S3板和USB数据线、Adafruit PID4885与PID326 2020自动复位版、短杜邦线、固定IDF5.5.5、可信LAN与电脑HTTP收集器
版本组合
框架:ESP-IDF5.5.5 / FreeRTOS SMP与UNICORE传感器:Sensirion C 1.1.0 / commit8e16a390…显示:IDF内置SSD1306 / 0x3D / 页打包
适用范围

DevKitC-1 v1.1 / N8R8;IDF5.5.5、8MB Flash、桥接UART115200、不启用PSRAM;SHT40 PID4885 0x44、SSD1306 PID326 2020版0x3D,共用SDA8/SCL9 100kHz;原生双核及同板UNICORE构建。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/idf-systems.md#e04;固定上游与实际源码比对摘要。验证日期:
构建 / 配置已通过
docs/esp32-evidence/logs/e04-build.log;真实Exit0;变体/源/锁/日志/固件SHA见idf-systems-builds.json。对象:DevKitC-1 v1.1 / WROOM-1-N8R8版本:ESP-IDF5.5.5验证日期:
仿真不适用
未运行ESP32/触摸/文件系统器件仿真;主机只执行固件共享的纯C模型与文件故障夹具,不证明调度、电气或断电可靠性。
本站实板待验证
尚无这套固件烧录、串口、外设或断电测试;observations.csv实测字段为空。

构建通过只说明指定环境中的代码或配置通过检查;实物功能、功耗和稳定性以对应的实板记录为准。

01 / 任务与队列

让任务通过数据副本协作

一个串行循环里,HTTP慢了会拖住下一次采样,OLED刷新慢了也会拖住网络。本例用采样、显示、网络三个任务,把完整sample_t分别复制进两个队列,而不是让两个消费者争抢同一队列。

采样每2秒产生一个带sequence、单调sampled_ms、原始温湿度和error的条目。显示只追最新状态;网络保留最多4个待处理条目,满时丢弃新条目并计数。有限RAM不能让无限慢的消费者永久不丢样。

作者已完成默认双核和单核+网络实现的两个真实构建。没有实体调度延迟、采样周期、栈余量或单核/双核性能数据,后面的CSV是空模板。

参考:[1] IDF5.5.5队列API:复制、阻塞时间和长度1 overwrite[2] IDF5.5.5 FreeRTOS:SMP/单核、任务创建与栈单位[3] IDF5.5.5 task.h:high-water mark单位为bytes

02 / 任务与队列

先接好两个具体模块,再讨论并发

指定官方ESP32-S3-DevKitC-1 v1.1 / WROOM-1-N8R8、Adafruit SHT40 PID4885、Adafruit PID326 128×64 SSD1306的2020自动复位版。GPIO8为SDA、GPIO9为SCL,两个模块共用100kHz总线和3.3V电源。

PID326按默认I2C跳线与默认地址0x3D使用,不能从其他128×64小屏猜成0x3C。旧版要额外RST接法,不在本例范围;SHT40地址0x44,3Vo保持不接。两个模块自带上拉,先用短线,不随意再加一组上拉。

完全断电后按表逐端连接;GPIO数字是信号号,不是随手数排针位置。USB仅接板子的USB-to-UART接口,日志115200;不向GPIO接5V。

DevKit信号PID4885 SHT40PID326 OLED
3V3VINVin
GNDGNDGND
GPIO8 / SDASDAData
GPIO9 / SCLSCLClk
不接3VoRST(仅限2020自动复位版)

参考:[4] IDF5.5.5 SSD1306:页打包、draw范围与I2C原生示例[5] Sensirion SHT4x C驱动1.1.0固定commit:测量与CRC[6] SHT40 PID4885模块供电与I2C引脚[7] PID326 128×64 OLED:I2C、地址及2020自动复位版

03 / 任务与队列

队列满时,策略必须写在API旁边

显示队列长度严格为1,生产者用xQueueOverwrite;旧画面尚未取走时,新样本覆盖它。显示消费者用相邻实际收到的sequence计算跳过数,首次收到不声称知道此前丢了多少;使用无符号差处理短序号回绕,不从uxQueueMessagesWaiting快照推算精确覆盖次数。

网络队列长度4,用xQueueSend(...,0);满就丢新条目,不让生产者等待网络。dropped_new由生产者独占更新并打印,条目里的值是入队时的计数快照;自身被丢的那条不会凭空出现在下游。

离线消费者仍取出并丢弃条目,之后再等3秒,所以只是有界瞬时RAM排队,不是断网持久缓存或断点补传。sample_json在消费时检查5秒新鲜度、CRC返回error和应用范围;过旧/无效温湿度写null,不用旧值冒充当前值。新鲜度是在序列化时判断,服务器收到时还可能更旧。

参考:[1] IDF5.5.5队列API:复制、阻塞时间和长度1 overwrite[5] Sensirion SHT4x C驱动1.1.0固定commit:测量与CRC[8] IDF5.5.5 HTTP客户端:perform与网络操作超时

04 / 任务与队列

队列拆任务,不会让一根I²C线变成并行总线

初始化先建总线、SHT设备和SSD1306,再创建任务。采样任务独占Sensirion驱动状态,显示任务独占OLED帧缓冲;一个FreeRTOS mutex保护整次SHT测量或整次OLED绘制。两者仍串行使用物理总线。

获取mutex等待最多500ms,SHT HAL每次I2C操作100ms,OLED I/O事务超时300ms。显示消费者释放mutex后才额外等默认5000ms;若把这5秒放在锁内,就人为挡住了采样任务。API操作超时不是整个任务周期的严格上限。

HTTP消费者每轮另等默认3000ms,HTTP的timeout_ms=3000是网络操作超时,慢响应可使整轮更久,未实现端到端总deadline。本例只向自己的可信局域网明文HTTP收集器发送,不带令牌,也不把该做法外推互联网安全传输。

参考:[1] IDF5.5.5队列API:复制、阻塞时间和长度1 overwrite[2] IDF5.5.5 FreeRTOS:SMP/单核、任务创建与栈单位[4] IDF5.5.5 SSD1306:页打包、draw范围与I2C原生示例[5] Sensirion SHT4x C驱动1.1.0固定commit:测量与CRC[8] IDF5.5.5 HTTP客户端:perform与网络操作超时

05 / 任务与队列

完整入口与安装运行

下载包包含未经修改的Sensirion C驱动1.1.0固定提交、MIT原生IDF HAL、OLED页缓冲绘制、网络模块、共享策略、lock与配置。不是Arduino库塞进IDF工程。OLED四行S/T/H/G分别是序号、温度℃、裁边后RH%和本次接收间跳样数,ERR表示该次绘制时样本无效。

OLED保留上次绘制快照,等待5秒期间不会自己重新计算年龄;串口序号/时间用于判断快照,不用这块故意慢的屏做实时安全告警。网络模块有占位值时CONFIG_MISSING且不尝试Wi-Fi,真实凭据仅填本地menuconfig,不进源码和日志。

在项目根执行下列命令。Demo configuration填写自己的WPA2网络和电脑局域网IPv4的http://地址;192.0.2.1是文档地址,不能直接当可达服务器。

代码 / 配置
idf.py --version
# 必须为ESP-IDF v5.5.5
idf.py set-target esp32s3
idf.py menuconfig
idf.py build
idf.py -p REPLACE_WITH_UART_PORT flash monitor
# 另在可信局域网电脑执行(需放行自己选择的本地端口):
python3 collector.py --bind 0.0.0.0 --port 8000

参考:[4] IDF5.5.5 SSD1306:页打包、draw范围与I2C原生示例[5] Sensirion SHT4x C驱动1.1.0固定commit:测量与CRC[8] IDF5.5.5 HTTP客户端:perform与网络操作超时

06 / 任务与队列

主循环全文与同源策略检查

三个工作任务都通过xTaskCreate创建,不固定到某个核。默认优先级采样5、显示4、网络3;栈参数按IDF的字节单位分别4096/4096/6144。这是配置,不是测得的峰值。app_main每10秒打印三个任务的minimum-free-stack bytes,作为运行期间曾经最小剩余栈,不能当调度延迟。

host/host_check.c直接包含固件使用的pipeline_policy.h,实际检查序号回绕/倒序保护、5000ms边界、未来时间、SHT范围、错误与陈旧null、RH裁边和序列化buffer不足。它不执行FreeRTOS队列、I2C或MCU调度;局域网收集器的本机HTTP夹具也不证明ESP通信。

代码 / 配置
/* SPDX-License-Identifier: MIT */
#include <inttypes.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_http_client.h"
#include "sdkconfig.h"
#include "sensirion_hal_idf.h"
#include "sensirion_i2c_hal.h"
#include "sht4x_i2c.h"
#include "net.h"
#include "oled.h"
#include "pipeline_policy.h"
static const char *TAG="queue_pipeline";
static QueueHandle_t display_queue, network_queue;
static SemaphoreHandle_t i2c_mutex;
static TaskHandle_t sampler_handle, display_handle, network_handle;
static void sample_task(void *unused) {
 (void)unused; uint32_t sequence=0, drops=0; TickType_t next=xTaskGetTickCount();
 for(;;) {
  sample_t sample={.sequence=++sequence,.error=-1,.dropped_new=drops};
  if(xSemaphoreTake(i2c_mutex,pdMS_TO_TICKS(500))==pdTRUE) {
   if(demo_i2c_ready()) sample.error=sht4x_measure_high_precision(&sample.temperature_milli,&sample.humidity_milli);
   xSemaphoreGive(i2c_mutex);
  }
  sample.sampled_ms=esp_timer_get_time()/1000;
  xQueueOverwrite(display_queue,&sample); /* length one: freshest state wins */
  if(xQueueSend(network_queue,&sample,0)!=pdTRUE) { ++drops; ESP_LOGW(TAG,"NETWORK_QUEUE_FULL drop-new sequence=%"PRIu32" total=%"PRIu32,sequence,drops); }
  ESP_LOGI(TAG,"SAMPLE sequence=%"PRIu32" error=%d producer_drop_new=%"PRIu32,sequence,sample.error,drops);
  xTaskDelayUntil(&next,pdMS_TO_TICKS(2000));
 }
}
static void display_task(void *unused) {
 (void)unused; sample_t sample; uint32_t previous=0; bool seen=false;
 for(;;) if(xQueueReceive(display_queue,&sample,portMAX_DELAY)==pdTRUE) {
  uint32_t gap=seen?sequence_gap(previous,sample.sequence):0; previous=sample.sequence; seen=true;
  esp_err_t e=ESP_ERR_TIMEOUT;
  if(xSemaphoreTake(i2c_mutex,pdMS_TO_TICKS(500))==pdTRUE) {
   e=oled_sample(&sample,esp_timer_get_time()/1000,gap); xSemaphoreGive(i2c_mutex);
  }
  ESP_LOGI(TAG,"DISPLAY sequence=%"PRIu32" skipped_between_receives=%"PRIu32" I2C=%s",sample.sequence,gap,esp_err_to_name(e));
  /* Delay outside the mutex: demonstrate queue policy without owning the bus. */
  vTaskDelay(pdMS_TO_TICKS(CONFIG_DEMO_DISPLAY_DELAY_MS));
 }
}
static void network_task(void *unused) {
 (void)unused; sample_t sample;
 for(;;) if(xQueueReceive(network_queue,&sample,portMAX_DELAY)==pdTRUE) {
  char body[320]; int64_t now=esp_timer_get_time()/1000;
  if(!sample_json(&sample,now,body,sizeof(body))) { ESP_LOGE(TAG,"serialization overflow"); continue; }
  if(net_online()) {
   esp_http_client_config_t config={.url=CONFIG_DEMO_HTTP_URL,.method=HTTP_METHOD_POST,.timeout_ms=3000,.disable_auto_redirect=true};
   esp_http_client_handle_t client=esp_http_client_init(&config);
   if(client) {
    esp_http_client_set_header(client,"Content-Type","application/json");
    esp_http_client_set_post_field(client,body,(int)strlen(body));
    esp_err_t e=esp_http_client_perform(client); int status=esp_http_client_get_status_code(client);
    ESP_LOGI(TAG,"HTTP sequence=%"PRIu32" result=%s status=%d",sample.sequence,esp_err_to_name(e),status);
    esp_http_client_cleanup(client);
   } else ESP_LOGE(TAG,"HTTP allocation failed");
  } else ESP_LOGW(TAG,"OFFLINE discard sequence=%"PRIu32" (no persistent replay)",sample.sequence);
  vTaskDelay(pdMS_TO_TICKS(CONFIG_DEMO_NETWORK_DELAY_MS));
 }
}
void app_main(void) {
 i2c_mutex=xSemaphoreCreateMutex(); display_queue=xQueueCreate(1,sizeof(sample_t)); network_queue=xQueueCreate(4,sizeof(sample_t));
 if(!i2c_mutex || !display_queue || !network_queue) { ESP_LOGE(TAG,"queue/mutex allocation failed"); return; }
 sensirion_i2c_hal_init(); sht4x_init(0x44);
 esp_err_t e=oled_init(demo_i2c_bus()); ESP_LOGI(TAG,"OLED init %s (physical result still unverified)",esp_err_to_name(e));
 net_start();
 if(xTaskCreate(sample_task,"sample",4096,NULL,5,&sampler_handle)!=pdPASS ||
    xTaskCreate(display_task,"display",4096,NULL,4,&display_handle)!=pdPASS ||
    xTaskCreate(network_task,"network",6144,NULL,3,&network_handle)!=pdPASS) {
  ESP_LOGE(TAG,"task allocation failed; stopping created workers");
  if(sampler_handle) vTaskDelete(sampler_handle);
  if(display_handle) vTaskDelete(display_handle);
  if(network_handle) vTaskDelete(network_handle);
  return;
 }
 ESP_LOGI(TAG,"cores=%d tasks unpinned; display queue=latest(1), network=drop-new(4)",portNUM_PROCESSORS);
 for(;;) {
  vTaskDelay(pdMS_TO_TICKS(10000));
  ESP_LOGI(TAG,"minimum-free-stack bytes sample=%u display=%u network=%u; not scheduling latency",(unsigned)uxTaskGetStackHighWaterMark(sampler_handle),(unsigned)uxTaskGetStackHighWaterMark(display_handle),(unsigned)uxTaskGetStackHighWaterMark(network_handle));
 }
}

参考:[1] IDF5.5.5队列API:复制、阻塞时间和长度1 overwrite[2] IDF5.5.5 FreeRTOS:SMP/单核、任务创建与栈单位[3] IDF5.5.5 task.h:high-water mark单位为bytes[5] Sensirion SHT4x C驱动1.1.0固定commit:测量与CRC[8] IDF5.5.5 HTTP客户端:perform与网络操作超时

07 / 任务与队列

主动减慢,再按同一条件比较单核和双核

先记录默认显示5秒、网络3秒的日志,观察DISPLAY实际序号间隔、NETWORK_QUEUE_FULL、producer_drop_new和最小剩余栈。再把两种延迟都改0重建,比较同一板同一网络条件;不能把日志打印本身的耗时忽略掉。

电脑启动collector.py,先用curl向localhost发一条合法JSON验证服务端;再填写电脑LAN地址让板发送,保留电脑接收序号与板HTTP状态。关闭收集器、断网、恢复,确认采样仍有日志、满队列策略生效,旧条目不被标作新鲜。实体网络步骤当前均pending。

单核变体使用同一ESP32-S3并开启CONFIG_FREERTOS_UNICORE=y,不把它叫成另一款单核芯片。新build目录叠加sdkconfig.defaults和sdkconfig.single-core;对照日志cores=1/2、相同负载下的采样间隔/栈/掉样。两个配置都编译成功只能证明构建支持,不能得出双核一定更快。

对照项目记录方式当前状态
慢/快消费者相同固件配置、序号、uptime、队列满与I2C错误实板pending
单核/双核CONFIG_FREERTOS_UNICORE、cores日志、同一负载两构建通过;指标pending
HTTP关闭/断网/恢复板状态、collector序号、丢新计数、样本年龄本机服务夹具通过;ESPpending
栈余量运行时minimum-free-stack bytes,覆盖最慢请求pending

参考:[1] IDF5.5.5队列API:复制、阻塞时间和长度1 overwrite[2] IDF5.5.5 FreeRTOS:SMP/单核、任务创建与栈单位[3] IDF5.5.5 task.h:high-water mark单位为bytes[8] IDF5.5.5 HTTP客户端:perform与网络操作超时

08 / 任务与队列

出错先保留证据,再扩大工程

屏幕失败但串口还有SAMPLE时先查0x3D、I2C模式和2020版本;SHT报错先断电核对0x44接线与CRC来源,不能用平滑掩盖断线。HTTP失败先测试电脑collector,再查Wi-Fi/IP/端口,不靠加大队列隐藏不可达服务。

纯策略检查命令为cc -std=c11 -Wall -Wextra -Werror -I main host/host_check.c -o /tmp/e04-host,再执行/tmp/e04-host。实际单核网络构建使用公开无效测试凭据并独立nm确认Wi-Fi/HTTP符号,没有进行射频联网。

将这次observations.csv填全后再增加离线存储或重传协议。原创工程和图MIT;main/sensirion九个原始文件保留BSD-3-Clause,IDF等依赖保留上游许可,不把库许可改写成本站许可。

参考:[1] IDF5.5.5队列API:复制、阻塞时间和长度1 overwrite[4] IDF5.5.5 SSD1306:页打包、draw范围与I2C原生示例[5] Sensirion SHT4x C驱动1.1.0固定commit:测量与CRC[8] IDF5.5.5 HTTP客户端:perform与网络操作超时

BACK TO THE SOURCE

资料来源与版本说明

固定SDK、组件源码和具体板级已核对;真实构建与同源主机检查已留证,触摸、I2C、联网、存储断电和调度实板结果均未补造。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. IDF5.5.5队列API:复制、阻塞时间和长度1 overwrite
  2. IDF5.5.5 FreeRTOS:SMP/单核、任务创建与栈单位
  3. IDF5.5.5 task.h:high-water mark单位为bytes
  4. IDF5.5.5 SSD1306:页打包、draw范围与I2C原生示例
  5. Sensirion SHT4x C驱动1.1.0固定commit:测量与CRC
  6. SHT40 PID4885模块供电与I2C引脚
  7. PID326 128×64 OLED:I2C、地址及2020自动复位版
  8. IDF5.5.5 HTTP客户端:perform与网络操作超时