让任务通过数据副本协作
一个串行循环里,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
队列满时,策略必须写在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与网络操作超时
队列拆任务,不会让一根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与网络操作超时
完整入口与安装运行
下载包包含未经修改的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与网络操作超时
主循环全文与同源策略检查
三个工作任务都通过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与网络操作超时
主动减慢,再按同一条件比较单核和双核
先记录默认显示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与网络操作超时
出错先保留证据,再扩大工程
屏幕失败但串口还有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与网络操作超时
资料来源与版本说明
固定SDK、组件源码和具体板级已核对;真实构建与同源主机检查已留证,触摸、I2C、联网、存储断电和调度实板结果均未补造。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。