2026 M-Design设计竞赛 - 基于 ESP32-C61 开发板的 SHT40 温湿度传感器设计
该项目使用了ESP32-C61,实现了温湿度传感器的设计,它的主要功能为:实时获取当前环境的温度和湿度。
标签
嵌入式系统
M-Design
ESP32-C61
helen
更新2026-06-09
4


基于 ESP32-C61 开发板的 SHT40 温湿度传感器设计

一、项目介绍和创意介绍

本项目的主题是“智能家居:基于 ESP32-C61 开发板的 SHT40 温湿度传感器设计”。从应用角度看,温湿度是家庭环境感知中最基础、最实用的一类数据。无论是卧室、书房、婴儿房、储藏间还是阳台花卉养护场景,用户都希望能够随时了解当前空气状态,并进一步联动空调、新风、加湿器、除湿机等设备,实现更加舒适和节能的居住体验。

本项目的核心创意并不是单纯“把传感器数值读出来”,而是把“环境采集 + 无线联网 + 本地网页实时展示”整合成一个轻量级、可扩展的智能家居节点。系统基于 ESP32-C61 开发板作为主控,使用 SHT40 传感器采集温湿度数据,通过 Wi-Fi 接入家庭局域网,并在开发板内部运行一个轻量 HTTP 服务器。用户无需安装额外 App,只需让手机或电脑与开发板处于同一网络,直接在浏览器中访问设备 IP 地址,就可以实时查看温度和湿度变化。

这样的设计具有几个明显优势。第一,部署简单,开发板上电联网后即可工作;第二,交互门槛低,浏览器就是界面;第三,后续扩展空间大,可以继续叠加历史数据存储、异常告警、阈值联动、云端同步以及多房间组网等功能。因此,这个项目既适合作为智能家居入门作品,也具备继续工程化演进的潜力。

二、使用到的硬件介绍

1. ESP32-C61 开发板

ESP32-C61 是乐鑫推出的一款具备 Wi-Fi 6、Bluetooth LE 以及 802.15.4 能力的低功耗 SoC,适合智能家居、物联网边缘节点等应用场景。本项目中,ESP32-C61 主要承担以下任务:

  1. 作为主控制器完成系统初始化。
  2. 通过 I2C 总线与 SHT40 传感器通信。
  3. 通过 Wi-Fi 连接家庭路由器。
  4. 运行嵌入式 Web 服务器和 WebSocket 服务。
  5. 定时采集并对外发布温湿度数据。

ESP32-C61 之所以适合该项目,一方面是因为其网络能力强,方便做本地网页展示或后续云接入;另一方面是 ESP-IDF 开发框架成熟,组件生态完善,适合快速完成原型设计和后续迭代。

2. SHT40 温湿度传感器

SHT40 是 Sensirion 推出的数字温湿度传感器,具有精度高、响应快、体积小、接口简单等特点。它采用 I2C 接口与主控通信,适合智能家居环境监测类应用。本项目中,SHT40 的优势主要体现在以下几点:

  1. 温湿度数据输出数字化,避免模拟量采集中的噪声影响。
  2. I2C 接口简单,只需 SDA、SCL 两根信号线即可完成通信。
  3. 器件功耗较低,适合后续低功耗场景扩展。
  4. 自带 CRC 校验机制,便于提高数据可靠性。

3. 硬件连接方式

SHT40 使用 I2C0 与 ESP32-C61 通信,连接关系如下:

  1. SDA 连接到 GPIO6。
  2. SCL 连接到 GPIO7。
  3. 传感器 I2C 地址使用 0x44。
  4. I2C 内部上拉在软件中启用。

三、方案框图和项目设计思路介绍

1. 系统方案框图

2. 设计思路

本项目采用“采集层、数据层、通信层、展示层”分层设计思路。

  1. 采集层:由 SHT40 和 I2C 驱动构成,负责定时获取温湿度原始数据。
  2. 数据层:通过共享数据结构缓存当前最新测量值,并使用互斥锁保证不同任务之间的数据一致性。
  3. 通信层:ESP32-C61 以 Wi-Fi Station 模式接入路由器,设备联网成功后启动 HTTP 服务器和 WebSocket 服务。
  4. 展示层:浏览器访问开发板 IP 后加载内置网页,网页通过 WebSocket 实时接收 JSON 格式数据并刷新显示。

这种设计的好处是模块职责清晰,传感器采集逻辑和网页展示逻辑互不干扰。即便后续要把网页改成 MQTT 上云、蓝牙广播或者接入 Home Assistant,也只需要替换部分通信或展示模块,而不必大幅修改底层采集驱动。

四、软件流程图和关键代码介绍

1. 软件总体流程图

2. 关键代码说明

(1)Wi-Fi 连接流程

在主程序中,设备工作在 Wi-Fi Station 模式。系统通过事件回调处理三类关键状态:启动连接、断线重连、获取 IP。断线后会按照设定的最大重试次数自动重新连接,这种设计能够提高系统在家庭网络环境下的稳定性。联网成功后,程序会打印获取到的 IP 地址,作为浏览器访问入口。

这里的设计亮点在于:Wi-Fi 连接并不是简单阻塞式等待,而是通过事件组同步系统状态。这样既符合 ESP-IDF 的事件驱动风格,也方便后续继续扩展更多网络状态处理逻辑,比如联网失败后的配网模式切换等。

(2)SHT40 采集任务

项目使用独立任务完成传感器采样。任务启动时先初始化 I2C 主总线,再完成 SHT40 驱动初始化,并读取一次传感器序列号用于设备识别。之后每隔 2 秒进行一次高精度测量,成功时把温度和湿度写入共享数据区,失败时打印错误日志。

该模块的实现体现了两个工程细节:

  1. 驱动中加入了 CRC 校验,能够发现总线传输异常或数据损坏问题。
  2. 测量延时根据 SHT40 不同精度模式分别设置,符合数据手册要求,保证了采样可靠性。

(3)共享数据设计

为了让采样任务和 Web 服务器任务安全访问同一份数据,项目定义了一个全局的 sensor_data_t 结构体,内部包含温度、湿度、有效标志位以及互斥锁。写入数据时先上锁,读取数据时同样先上锁,从而避免任务切换造成的数据竞争。

这部分虽然代码量不大,却是系统稳定运行的关键。如果没有互斥锁保护,就可能出现网页读取到一半更新中的数据,导致显示异常甚至隐蔽 bug。

(4)Web 服务器与网页实时显示

联网成功后,系统启动内置 HTTP 服务器。根路径 / 返回一份嵌入在 C 字符串中的 HTML 页面,页面包含温度卡片、湿度卡片以及连接状态显示。网页加载后会自动建立 WebSocket 连接,请求 /ws 接口;开发板端则由广播任务每 2 秒向所有在线客户端发送一帧 JSON 数据,例如:

{"t":25.36,"h":58.42}

前端收到数据后,使用 JavaScript 动态更新页面中的温度和湿度数值。相比传统轮询方式,WebSocket 能显著降低通信开销,并让数据刷新更加及时,适合实时监测类页面。

3. 关键代码价值总结

从软件架构上看,本项目最核心的代码价值主要体现在以下三个方面:

  1. 基于 ESP-IDF 的事件驱动式 Wi-Fi 联网机制。
  2. 带 CRC 校验的 SHT40 驱动封装,提高了采集可靠性。
  3. 通过 HTTP + WebSocket 实现嵌入式设备到浏览器的实时数据可视化。


传感器SHT40驱动代码

/*
 * SHT4x Temperature & Humidity Sensor Driver Implementation
 * Based on Sensirion SHT4x Datasheet Version 6.4
 */

#include "sht4x.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <string.h>

static const char *TAG = "sht4x";

/* CRC-8 parameters from datasheet Table 7:
 * Polynomial: 0x31, Init: 0xFF, Final XOR: 0x00 */
static uint8_t sht4x_crc8(const uint8_t *data, size_t len)
{
    uint8_t crc = 0xFF;
    for (size_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (int bit = 0; bit < 8; bit++) {
            if (crc & 0x80) {
                crc = (crc << 1) ^ 0x31;
            } else {
                crc = crc << 1;
            }
        }
    }
    return crc;
}


static esp_err_t sht4x_send_cmd(sht4x_t *dev, uint8_t cmd)
{
    return i2c_master_transmit(dev->i2c_dev, &cmd, 1, 100);
}


static esp_err_t sht4x_read_response(sht4x_t *dev, uint8_t *data, size_t len)
{
    return i2c_master_receive(dev->i2c_dev, data, len, 100);
}


/* Parse 6 bytes: [T_MSB, T_LSB, T_CRC, RH_MSB, RH_LSB, RH_CRC] */
static esp_err_t sht4x_parse_measurement(const uint8_t *raw, float *temperature, float *humidity)
{
    /* Verify temperature CRC */
    uint8_t crc_t = sht4x_crc8(&raw[0], 2);
    if (crc_t != raw[2]) {
        ESP_LOGE(TAG, "Temperature CRC mismatch: calc=0x%02X, recv=0x%02X", crc_t, raw[2]);
        return ESP_ERR_INVALID_CRC;
    }


    /* Verify humidity CRC */
    uint8_t crc_rh = sht4x_crc8(&raw[3], 2);
    if (crc_rh != raw[5]) {
        ESP_LOGE(TAG, "Humidity CRC mismatch: calc=0x%02X, recv=0x%02X", crc_rh, raw[5]);
        return ESP_ERR_INVALID_CRC;
    }


    /* Convert raw values using datasheet formulas (section 4.6) */
    uint16_t t_ticks = ((uint16_t)raw[0] << 8) | raw[1];
    uint16_t rh_ticks = ((uint16_t)raw[3] << 8) | raw[4];


    /* T = -45 + 175 * t_ticks / 65535 */
    *temperature = -45.0f + 175.0f * (float)t_ticks / 65535.0f;


    /* RH = -6 + 125 * rh_ticks / 65535, clamped to [0, 100] */
    float rh = -6.0f + 125.0f * (float)rh_ticks / 65535.0f;
    if (rh > 100.0f) rh = 100.0f;
    if (rh < 0.0f) rh = 0.0f;
    *humidity = rh;


    return ESP_OK;
}


esp_err_t sht4x_init(sht4x_t *dev, const sht4x_config_t *config)
{
    if (dev == NULL || config == NULL) {
        return ESP_ERR_INVALID_ARG;
    }


    i2c_device_config_t dev_cfg = {
        .dev_addr_length = I2C_ADDR_BIT_LEN_7,
        .device_address = config->i2c_addr,
        .scl_speed_hz = 400000,  /* Fast mode 400 kHz */
    };


    esp_err_t ret = i2c_master_bus_add_device(config->i2c_bus, &dev_cfg, &dev->i2c_dev);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to add I2C device: %s", esp_err_to_name(ret));
        return ret;
    }


    /* Soft reset to ensure known state */
    ret = sht4x_soft_reset(dev);
    if (ret != ESP_OK) {
        ESP_LOGW(TAG, "Soft reset failed (sensor may not be connected): %s", esp_err_to_name(ret));
    }


    ESP_LOGI(TAG, "SHT4x initialized at address 0x%02X", config->i2c_addr);
    return ESP_OK;
}


esp_err_t sht4x_soft_reset(sht4x_t *dev)
{
    esp_err_t ret = sht4x_send_cmd(dev, 0x94);
    if (ret != ESP_OK) return ret;
    /* Wait max 1 ms for reset (datasheet Table 5).
     * +1 to guarantee minimum delay when tick period > 1ms */
    vTaskDelay(pdMS_TO_TICKS(2) + 1);
    return ESP_OK;
}


esp_err_t sht4x_read_serial(sht4x_t *dev, uint32_t *serial)
{
    if (dev == NULL || serial == NULL) {
        return ESP_ERR_INVALID_ARG;
    }


    esp_err_t ret = sht4x_send_cmd(dev, 0x89);
    if (ret != ESP_OK) return ret;


    vTaskDelay(pdMS_TO_TICKS(2) + 1);


    uint8_t raw[6];
    ret = sht4x_read_response(dev, raw, 6);
    if (ret != ESP_OK) return ret;


    /* Verify CRCs for both 16-bit words */
    if (sht4x_crc8(&raw[0], 2) != raw[2]) {
        ESP_LOGE(TAG, "Serial number word 1 CRC mismatch");
        return ESP_ERR_INVALID_CRC;
    }
    if (sht4x_crc8(&raw[3], 2) != raw[5]) {
        ESP_LOGE(TAG, "Serial number word 2 CRC mismatch");
        return ESP_ERR_INVALID_CRC;
    }


    *serial = ((uint32_t)raw[0] << 24) | ((uint32_t)raw[1] << 16) |
              ((uint32_t)raw[3] << 8) | raw[4];
    return ESP_OK;
}


esp_err_t sht4x_measure(sht4x_t *dev, sht4x_repeatability_t repeatability,
                         float *temperature, float *humidity)
{
    if (dev == NULL || temperature == NULL || humidity == NULL) {
        return ESP_ERR_INVALID_ARG;
    }


    esp_err_t ret = sht4x_send_cmd(dev, (uint8_t)repeatability);
    if (ret != ESP_OK) return ret;


    /* Wait for measurement to complete (datasheet Table 5).
     * +1 to guarantee minimum delay when tick period > 1ms */
    switch (repeatability) {
        case SHT4X_REPEATABILITY_HIGH:
            vTaskDelay(pdMS_TO_TICKS(10) + 1);  /* max 8.3 ms */
            break;
        case SHT4X_REPEATABILITY_MEDIUM:
            vTaskDelay(pdMS_TO_TICKS(5) + 1);   /* max 4.5 ms */
            break;
        case SHT4X_REPEATABILITY_LOW:
            vTaskDelay(pdMS_TO_TICKS(2) + 1);   /* max 1.6 ms */
            break;
    }


    uint8_t raw[6];
    ret = sht4x_read_response(dev, raw, 6);
    if (ret != ESP_OK) return ret;


    return sht4x_parse_measurement(raw, temperature, humidity);
}


esp_err_t sht4x_heater_measure(sht4x_t *dev, sht4x_heater_cmd_t heater_cmd,
                                float *temperature, float *humidity)
{
    if (dev == NULL || temperature == NULL || humidity == NULL) {
        return ESP_ERR_INVALID_ARG;
    }


    esp_err_t ret = sht4x_send_cmd(dev, (uint8_t)heater_cmd);
    if (ret != ESP_OK) return ret;


    /* Determine wait time based on heater command */
    uint32_t wait_ms;
    switch (heater_cmd) {
        case SHT4X_HEATER_200MW_1S:
        case SHT4X_HEATER_110MW_1S:
        case SHT4X_HEATER_20MW_1S:
            wait_ms = 1100;  /* 1s heater + measurement */
            break;
        case SHT4X_HEATER_200MW_100MS:
        case SHT4X_HEATER_110MW_100MS:
        case SHT4X_HEATER_20MW_100MS:
            wait_ms = 120;   /* 0.1s heater + measurement */
            break;
        default:
            return ESP_ERR_INVALID_ARG;
    }


    vTaskDelay(pdMS_TO_TICKS(wait_ms));


    uint8_t raw[6];
    ret = sht4x_read_response(dev, raw, 6);
    if (ret != ESP_OK) return ret;


    return sht4x_parse_measurement(raw, temperature, humidity);
}


webserver,启动ws_broadcast_task任务监听websocket连接和数据收发。

#include <string.h>
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_http_server.h"
#include "esp_log.h"
#include "sensor_data.h"
#include "webserver.h"


static const char *TAG = "webserver";


/* Store server handle */
static httpd_handle_t s_server = NULL;


/* ---- Embedded HTML page ---- */
static const char index_html[] =
"<!DOCTYPE html>"
"<html><head><meta charset=\"UTF-8\">"
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
"<title>ESP32 Sensor Monitor</title>"
"<style>"
"*{margin:0;padding:0;box-sizing:border-box}"
"body{font-family:system-ui,-apple-system,sans-serif;background:#0f172a;color:#e2e8f0;"
"display:flex;justify-content:center;align-items:center;min-height:100vh}"
".container{text-align:center;padding:2rem}"
"h1{font-size:1.5rem;margin-bottom:2rem;color:#94a3b8}"
".cards{display:flex;gap:1.5rem;flex-wrap:wrap;justify-content:center}"
".card{background:#1e293b;border-radius:1rem;padding:2rem 3rem;min-width:200px;"
"box-shadow:0 4px 24px rgba(0,0,0,.3)}"
".card.temp{border-top:4px solid #f97316}"
".card.humi{border-top:4px solid #06b6d4}"
".label{font-size:.85rem;color:#94a3b8;margin-bottom:.5rem}"
".value{font-size:2.8rem;font-weight:700}"
".unit{font-size:1rem;color:#64748b;margin-left:.2rem}"
".temp .value{color:#fb923c}"
".humi .value{color:#22d3ee}"
".status{margin-top:1.5rem;font-size:.8rem;color:#475569}"
".status.connected{color:#22c55e}"
".status.disconnected{color:#ef4444}"
"</style></head><body>"
"<div class=\"container\">"
"<h1>&#127777; ESP32 Sensor Monitor</h1>"
"<div class=\"cards\">"
"<div class=\"card temp\">"
"<div class=\"label\">Temperature</div>"
"<div class=\"value\" id=\"temp\">--<span class=\"unit\">&deg;C</span></div>"
"</div>"
"<div class=\"card humi\">"
"<div class=\"label\">Humidity</div>"
"<div class=\"value\" id=\"humi\">--<span class=\"unit\">%%RH</span></div>"
"</div></div>"
"<div class=\"status disconnected\" id=\"status\">Connecting...</div>"
"</div>"
"<script>"
"var ws,st=document.getElementById('status');"
"function connect(){"
"ws=new WebSocket('ws://'+location.host+'/ws');"
"ws.onopen=function(){st.textContent='Connected';st.className='status connected'};"
"ws.onclose=function(){st.textContent='Disconnected';st.className='status disconnected';"
"setTimeout(connect,2000)};"
"ws.onerror=function(){ws.close()};"
"ws.onmessage=function(e){"
"var d=JSON.parse(e.data);"
"document.getElementById('temp').innerHTML=d.t.toFixed(1)+'<span class=\"unit\">&deg;C</span>';"
"document.getElementById('humi').innerHTML=d.h.toFixed(1)+'<span class=\"unit\">%%RH</span>';"
"}}"
"connect();"
"</script></body></html>";


/* ---- HTTP GET / handler ---- */
static esp_err_t index_get_handler(httpd_req_t *req)
{
    httpd_resp_set_type(req, "text/html");
    httpd_resp_send(req, index_html, sizeof(index_html) - 1);
    return ESP_OK;
}


/* ---- WebSocket handler ---- */


/*
 * Async send structure: we use httpd_queue_work to push data from the
 * sensor broadcast task into the httpd thread context.
 */
typedef struct {
    httpd_handle_t hd;
    int fd;
} ws_client_t;


#define MAX_WS_CLIENTS 4
static ws_client_t s_ws_clients[MAX_WS_CLIENTS];
static int s_ws_client_count = 0;


static esp_err_t ws_handler(httpd_req_t *req)
{
    /* First invocation for a new client: handshake */
    if (req->method == HTTP_GET) {
        ESP_LOGI(TAG, "WS handshake fd=%d", httpd_req_to_sockfd(req));
        /* Register client */
        if (s_ws_client_count < MAX_WS_CLIENTS) {
            s_ws_clients[s_ws_client_count].hd = req->handle;
            s_ws_clients[s_ws_client_count].fd = httpd_req_to_sockfd(req);
            s_ws_client_count++;
        }
        return ESP_OK;
    }


    /* Receive frame (we only expect pong / close) */
    httpd_ws_frame_t ws_pkt;
    memset(&ws_pkt, 0, sizeof(ws_pkt));
    ws_pkt.type = HTTPD_WS_TYPE_TEXT;


    esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0);
    if (ret != ESP_OK) {
        return ret;
    }
    /* We don't process incoming data, just acknowledge */
    return ESP_OK;
}


/* Remove disconnected clients */
static void ws_remove_client(int idx)
{
    if (idx < s_ws_client_count - 1) {
        s_ws_clients[idx] = s_ws_clients[s_ws_client_count - 1];
    }
    s_ws_client_count--;
}


/* Broadcast task: periodically sends sensor data to all WS clients */
static void ws_broadcast_task(void *arg)
{
    char buf[64];
    float temperature, humidity;


    while (1) {
        vTaskDelay(pdMS_TO_TICKS(2000));


        if (s_ws_client_count == 0 || s_server == NULL) {
            continue;
        }


        if (!sensor_data_get(&temperature, &humidity)) {
            continue;
        }


        int len = snprintf(buf, sizeof(buf), "{\"t\":%.2f,\"h\":%.2f}", temperature, humidity);


        httpd_ws_frame_t ws_pkt = {
            .final = true,
            .fragmented = false,
            .type = HTTPD_WS_TYPE_TEXT,
            .payload = (uint8_t *)buf,
            .len = len,
        };


        for (int i = s_ws_client_count - 1; i >= 0; i--) {
            esp_err_t ret = httpd_ws_send_frame_async(s_ws_clients[i].hd, s_ws_clients[i].fd, &ws_pkt);
            if (ret != ESP_OK) {
                ESP_LOGW(TAG, "WS send failed fd=%d, removing client", s_ws_clients[i].fd);
                ws_remove_client(i);
            }
        }
    }
}


/* ---- URI definitions ---- */
static const httpd_uri_t uri_index = {
    .uri       = "/",
    .method    = HTTP_GET,
    .handler   = index_get_handler,
};


static const httpd_uri_t uri_ws = {
    .uri          = "/ws",
    .method       = HTTP_GET,
    .handler      = ws_handler,
    .is_websocket = true,
};


/* ---- Public API ---- */
esp_err_t webserver_start(void)
{
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.max_open_sockets = MAX_WS_CLIENTS + 1;


    ESP_LOGI(TAG, "Starting HTTP server on port %d", config.server_port);
    esp_err_t ret = httpd_start(&s_server, &config);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to start HTTP server: %s", esp_err_to_name(ret));
        return ret;
    }


    httpd_register_uri_handler(s_server, &uri_index);
    httpd_register_uri_handler(s_server, &uri_ws);


    /* Start broadcast task */
    xTaskCreate(ws_broadcast_task, "ws_bcast", 4096, NULL, 5, NULL);


    ESP_LOGI(TAG, "HTTP server started");
    return ESP_OK;
}


void webserver_stop(void)
{
    if (s_server) {
        httpd_stop(s_server);
        s_server = NULL;
        s_ws_client_count = 0;
    }
}

主函数启动传感器读取任务,webserver任务,初始化WiFi



void app_main(void)
{
    //Initialize NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
      ESP_ERROR_CHECK(nvs_flash_erase());
      ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);


    /* Print chip information */
    esp_chip_info_t chip_info;
    uint32_t flash_size;
    esp_chip_info(&chip_info);
    printf("This is %s chip with %d CPU core(s), %s%s%s%s, ",
           CONFIG_IDF_TARGET,
           chip_info.cores,
           (chip_info.features & CHIP_FEATURE_WIFI_BGN) ? "WiFi/" : "",
           (chip_info.features & CHIP_FEATURE_BT) ? "BT" : "",
           (chip_info.features & CHIP_FEATURE_BLE) ? "BLE" : "",
           (chip_info.features & CHIP_FEATURE_IEEE802154) ? ", 802.15.4 (Zigbee/Thread)" : "");


    unsigned major_rev = chip_info.revision / 100;
    unsigned minor_rev = chip_info.revision % 100;
    printf("silicon revision v%d.%d, ", major_rev, minor_rev);
    if(esp_flash_get_size(NULL, &flash_size) != ESP_OK) {
        printf("Get flash size failed");
        return;
    }


    ESP_LOGI(TAG, "%" PRIu32 "MB %s flash\n", flash_size / (uint32_t)(1024 * 1024),
           (chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external");\


    ESP_LOGI(TAG, "Minimum free heap size: %" PRIu32 " bytes", esp_get_minimum_free_heap_size());


    if (CONFIG_LOG_MAXIMUM_LEVEL > CONFIG_LOG_DEFAULT_LEVEL) {
        /* If you only want to open more logs in the wifi module, you need to make the max level greater than the default level,
         * and call esp_log_level_set() before esp_wifi_init() to improve the log level of the wifi module. */
        esp_log_level_set("wifi", CONFIG_LOG_MAXIMUM_LEVEL);
    }


    /* Initialize shared sensor data */
    sensor_data_init();


    /* Start SHT40 sensor task (before WiFi, which blocks until connected) */
    xTaskCreate(sht40_task, "sht40_task", 4096, NULL, 5, NULL);


    ESP_LOGI(TAG, "ESP_WIFI_MODE_STA");
    wifi_init_sta();


    /* Start HTTP server with WebSocket after WiFi is connected */
    webserver_start();


    for (;;) {
        vTaskDelay(2000 / portTICK_PERIOD_MS);
        // ESP_LOGI(TAG, "free heap size: %" PRIu32 " bytes", esp_get_free_internal_heap_size());
    }
}

五、功能展示图及说明

  • 硬件实物连接:使用杜邦线连接传感器SHT40


  • 串口日志图:展示系统启动、联网成功、周期输出温湿度日志。

  • 上电启动,成功连接WiFi网络

  • 资源占用情况:开发板自带8MB flash,目前使用了765KB左右;内存使用了不到120KB。还有不少余量,为后面功能扩展也留有充足的空间

  • 浏览器页面:实时温度、湿度卡片和WebSocket连接状态。

  • 多终端访问:当前最大支持4个客户端同时访问

设备上电后自动连接到预设 Wi-Fi 网络,浏览器访问开发板获取到的 IP 地址,即可看到实时刷新温湿度数据的网页界面。页面采用卡片式布局显示温度和湿度,底部显示 WebSocket 连接状态。当传感器数据发生更新时,通过websocket网页端同步刷新,无需手动重载页面。

六、设计中遇到的难题和解决方法

1. Wi-Fi 联网稳定性问题

在物联网项目中,Wi-Fi 环境并不总是稳定的,路由器重启、信号波动、密码配置错误都可能导致设备无法正常工作。为解决这个问题,项目中使用事件回调配合重连计数方式,在掉线后自动尝试重新连接,并通过事件组区分“成功获取 IP”和“达到最大重试次数失败”两种状态。这种方式相比简单地死循环调用连接函数更规范,也更利于后续扩展。

2. 传感器通信可靠性问题

I2C 总线在连线较长、接触不良或者干扰较大时,容易出现通信异常。如果直接把读到的数据当作有效值使用,可能造成测量结果失真。针对这个问题,项目在 SHT40 驱动中加入了 CRC 校验逻辑,对温度和湿度两个数据字分别进行校验。如果 CRC 不匹配,就直接返回错误并记录日志,从源头上保证数据可靠性。

3. 多任务共享数据问题

传感器采样任务和 Web 服务广播任务会并发访问同一份温湿度数据。如果缺少同步保护,就有可能产生数据竞争。项目采用互斥锁机制封装共享数据读写接口,既保证了结构清晰,又避免了不同任务之间互相影响。

4. 前端与嵌入式结合问题

嵌入式项目往往更重视底层驱动,而忽略用户可视化体验。本项目在有限资源下把 HTML、CSS、JavaScript 页面直接嵌入固件中,结合 WebSocket 实现实时显示。这一过程中需要平衡页面效果、代码体积和通信效率。最终实现的网页虽然轻量,但已经具备较好的可读性和实时性,为后续升级为更完整的控制面板打下了基础。

七、对本次竞赛的心得体会、意见与建议

通过这次论坛活动,我对“从一个传感器出发,逐步搭建完整物联网应用”的过程有了更深的理解。以前做单片机项目时,往往更关注某一个功能点是否能跑通,例如是否能读到寄存器、是否能点亮 LED;而这次项目让我更加关注系统完整性,包括硬件接口、驱动可靠性、联网流程、前端展示和用户体验。这种“从功能验证走向系统设计”的思考方式,对以后做智能家居、边缘网关甚至更复杂的嵌入式产品都很有帮助。

本次项目也让我感受到 ESP32-C61 和 ESP-IDF 生态的成熟。一方面,官方框架对 Wi-Fi、事件循环、HTTP 服务器等基础能力支持完善,开发者可以把更多精力放在应用逻辑本身;另一方面,像 SHT40 这样的外设驱动也很适合通过组件化方式封装,便于复用和维护。对个人开发者来说,这大大降低了从想法到作品落地的门槛。

总体来说,这次竞赛不仅是一次作品提交,更像是一次完整的工程实践训练。通过本项目,我完成了从硬件连接、驱动编写、联网调试到网页可视化展示的全流程开发,也更加明确了未来可以继续深挖的方向,例如加入阈值告警、历史曲线、手机适配界面、Home Assistant 接入以及多传感器节点扩展等。可以说,这个项目虽然体量不大,但非常贴近真实智能家居产品的基本形态,具有较强的学习价值和继续迭代的意义。

八、总结

本项目以 ESP32-C61 开发板和 SHT40 温湿度传感器为基础,实现了一个面向智能家居场景的环境监测节点。系统能够完成温湿度采集、Wi-Fi 联网、本地网页展示和 WebSocket 实时推送,整体方案简洁、实用、可扩展。通过该项目,不仅验证了 ESP32-C61 在智能家居场景中的应用能力,也展示了嵌入式系统结合 Web 技术后所带来的直观交互优势。

如果后续继续完善,本项目完全可以从“课程/竞赛作品”进一步升级为“可落地的家庭环境监测终端”,并与更多智能家居设备形成联动,具备良好的应用前景。

附件下载
esp32-c61-sht4x.zip
esp-idf项目源码
团队介绍
个人电子爱好者
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号