海欧的博客

  • Navidrome
  1. 首页
  2. 嵌入式
  3. 正文

ESP32连接阿里云(ESP-IDF工具链的使用)

2026年3月7日 283点热度 0人点赞 0条评论

ESP32连接阿里云(ESP-IDF工具链的使用)

image-20260307005412389

1. 前言

谈到物联网那肯定会想到esp32,正因为esp32的完善的生态发展带动了物联网的普及,让设备上云已经不是遥不可及的事情了。我们借助物联网操作系统框架以及完善的开发生态,只需要少量代码就可以让设备连接互联网。那些繁重的网络设备驱动、网络协议、云平台就交给厂商,厂商提供了简单高效的接口以及一整套开发工具链,这样一来开发一款能上云的设备就简单多了。

目标是什么:掌握ESP-IDF工具链的使用,以最简单的方式,把嵌入式设备连接到云平台(阿里云)

目标芯片选型:目前确定为esp32c3,因为c3是性价比极高的芯片方案,白菜价格。当然如果有性能的要求,选择esp32s3就可以了

选择esp32有什么优势:有完善的生态,芯片自带wifi+蓝牙,一整套物联网解决方案,文档丰富

esp32开发方式的选择:esp-idf v5.5 ,代码高效体积小

2. 环境安装与使用

  1. 安装esp-idf工具链,CMake Ninja用于构建编译下载烧录,如果多个版本tool路径可以相同,避免重复下载
  2. 安装vscode,用于代码编辑
  3. 执行esp-idf powershell 命令窗口,已打包好环境变量,手动导出临时环境执行export.ps1
  4. cd 到项目工程目录
  5. idf.py create-project prj
    #创建项目 ,会创建CMakeLists构建配置与app_main程序prj.c
    #修改prj文件夹为自定义项目名称
    mv ./prj  ./esp32c3-mini-mqtt
  6. cd prj
    idf.py set-target esp32c3
    #设置芯片为esp32c3,生成sdkconfig的配置文件;生成build目录(链接配置)
  7. 使用vscode 打开项目文件,使用图形化执行build 执行flash monitor;使用图形化会自动创建.vscode/settings.json配置文件
  8. idf.py build #编译,生成bootloader partition-table prj 的bin文件
    idf.py -p COM7 flash monitor #烧录+串口监视器

    使用事项:

    1. 使用 idf.py fullclean 彻底清除项目的编译产物与CMake配置 (clean仅仅是清理o,bin文件)
    2. sdkconfig不要手动编辑,要使用menuconfig或者图形界面进行编辑
    3. vscode CPP编译器报红线; 配置:esp-idf:add vscode configuration folder;会创建c_cpp_properties.json文件,设置 CPP编译器查找路径

3. 强大的组件

内置组件在安装目录components下,项目组件在项目根目录的components下,托管组件在项目根目录的managed_components下

ESP Component Registry是官方的组件库,常用的组件都在这里,比如 espressif/led_strip

使用组件:idf.py add-dependency "espressif/led_strip^3.0.3" ,会main函数路径下生成idf_component.yml 文件,声明了组件的依赖

生效组件: idf.py reconfigure 触发组件管理器 或者 menuconfig 的操作,会执行下载组件并在顶层路径下生成managed_components 文件夹

image-20260303005949951

Main CMakeLists:我们在mian目录下会发现有一个CMakelists.txt,它定义了main组件编译规则

顶层CMakeLists:负责整个项目的构建,负责最后的链接生成

3.1 创建自定义组件

   idf.py create-component -C components wifi_task #第一步,创建组件
   # main/CMakeLists.txt
   idf_component_register(SRCS "prj.c"
                           INCLUDE_DIRS "."
                           REQUIRES wifi_task adc_task)
    # 链接组件;声明 main 依赖于 自定义创建组件,会优先编译自定义组件                        

4. 组织项目

  • 子模块创建为自定义组件(依赖清晰,main仅需REQUIRES即可)
  • 子模块归类到main组件下 (main设置SRC INCLUDE_DIRS)

    作为简单的应用代码且不需要复用,归类到main组件下是一个不错的方案

# main/CMakeLists.txt
idf_component_register(SRCS "prj.c"
                              "wifi_task/wifi_task.c"   # 列出main组件的源文件的相对路径
                              "adc_task/adc_task.c"
                        INCLUDE_DIRS "." 
                                      "wifi_task"        # 将子目录也加入头文件搜索路径
                                      "adc_task")

PS: 编译器头文件路径(-I 选项)不会递归搜索的,要显式声明多个路径

5. 量产烧录

烧录模式:拉低 BOOT→复位→释放 BOOT

其实使用idf.py falsh (底层调用了esptool.py)进行烧录,是烧录了app.bin / bootloader.bin / partition-table.bin,命令可以只烧录app

Flash下载工具:https://docs.espressif.com/projects/esp-test-tools/zh_CN/latest/esp32/production_stage/tools/flash_download_tool.html

方法一: flash_download_tool

image-20260304002309934

烧录地址查看:.buildflash_args文件

esp-idf环境下,cd到build目录下脚本代码引用"@flash_args"

python -m esptool --chip esp32c3 -b 460800 --before default_reset --after hard_reset write_flash "@flash_args"
image-20260304002642788

方法二: esptool.py

编写esptool.py脚本,可以多bin烧录,验证烧录,使用树莓派香橙派运行py脚本,适合产线自动化烧录

#生成合并BIN文件在build目录下
esptool.py --chip esp32c3 merge_bin 
-o merged-binary.bin 
-f raw --flash_mode dio 
--flash_freq 80m --flash_size 2MB 
0x0 bootloader/bootloader.bin 
0x10000 prj2.bin 
0x8000 partition_table/partition-table.bin

esptool.Py --chip esp32c3 -b 460800 
--before default_reset --after hard_reset 
write_flash --flash_mode dio --flash_size 2MB --flash_freq 80m 
0x0 buildbootloaderbootloader.bin 
0x8000 buildpartition_tablepartition-table.bin 
0x10000 buildprj2.bin

esptool.py --chip esp32c3 --port COM7 verify_flash 0x10000 prj.bin

6. 强大的分区表

默认分区表配置为Single factory app, no OTA 0x8000,使用components/partition_table/partitions_singleapp.csv (单app)分区表

分区表CSV定义了flash的布局,分区表bin用于bootloader与app的解析flash存储;

分区表bin文件通常只有几百个字节而已,存储了flash的布局;

image-20260304001818271
idf.py partition-table #打印分区表
image-20260304004856529

自定义分区表:存放在根目录下,命名为partitions.csv,在menuconfig配置中选择自定义的csv

Type = app(0x00) 时,SubType= {factory(0x00), ota_0(0x10), ota_1(0x11), ... , ota_15(0x1F), test (0x20) }

Type = data(0x01) 时,SubType= {ota(0x00), phy(0x01), nvs(0x02), nvs_keys(0x04),自定义(0x80-0xFE)}

bootloader(0x03) , CSV 分区表不会出现,硬件固定了烧录地址,gen_esp32part.py 工具的使用

partition_table (0x04) , CSV 分区表不会出现,硬件固定了烧录地址,partition_table 的大小固定为 0x1000的整数倍(4KB扇区)

自定义分区Type 类型取值: 0x40-0xFE

必须有的分区默认名称:nvs , phy_init , factory

扇区大小为 0x1000 (4 KB)

CSV 文件会编译为bin文件烧录到指定地址

idf.py partition-table-flash 仅烧录分区表,idf.py flash烧录所有;

esptool.py erase_flash 手动擦除flash

7.强大的事件循环

idf框架提供了低耦合的系统框架通讯方式--事件循环,是消息分发中心, 为解决系统组件模块间的事件通知与解耦

默认系统事件循环:(带default,不带to/with)esp_event_handler_instance_register()

自定义事件循环:(带to/with,不带default)esp_event_handler_register_with()

ESP_EVENT_DEFINE_BASE(TEMP_SENSOR_EVENTS_BASE); // 定义事件基
esp_event_loop_args_t event_loop_args = {      //定义循环参数
    .queue_size = 5,
    .task_name = "user_event_task",
    .task_priority = 5,
    .task_stack_size = 2048,
    .task_core_id = tskNO_AFFINITY
};
esp_event_loop_create(&event_loop_args, &my_event_loop) //创建循环
esp_event_handler_register_with(event_loop,event_base,event_id, //注册循环
                                event_handler,event_handler_arg,instance);
esp_event_post_to(event_loop,event_base,event_id,       //发布循环
                  event_data,event_data_size,ticks_to_wait);

使用freertos的通知/信号/队列 还是使用 事件循环:

  1. 如果涉及到系统组件的通讯,使用event loop,但开销高
  2. 如果不涉及到系统组件的通讯,使用 freertos 的通知/信号/队列

8. 连接到互联网

使用内置组件 esp_http_client 测试网络连通性(可选):

添加依赖:

idf_component_register(SRCS "main.c"
                       INCLUDE_DIRS "."
                       REQUIRES esp_http_client)

初始化wifi:

    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);
    wifi_event_group = xEventGroupCreate();

    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();

    // 初始化 Wi-Fi
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    // 注册事件处理
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        NULL));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                        IP_EVENT_STA_GOT_IP,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        NULL));
    // 设置 Wi-Fi 配置
    wifi_config_t wifi_config = {
        .sta = {
            .ssid = WIFI_SSID,
            .password = WIFI_PASS,
            .threshold.authmode = WIFI_AUTH_WPA2_PSK,
        },
    };
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());

    ESP_LOGI(TAG, "Wi-Fi initialization done.");

//wifi事件循环
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
                               int32_t event_id, void *event_data)
{
    //判断事件类型是否WIFI事件
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        ESP_LOGI(TAG, "Wi-Fi started, connecting...");
        esp_wifi_connect();
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        ESP_LOGI(TAG, "Wi-Fi disconnected, retry connection...");
        esp_wifi_connect();
        xEventGroupClearBits(wifi_event_group, WIFI_CONNECTED_BIT);
    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data;
        ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));
        ESP_LOGI(TAG, "Set WIFI_CONNECTED_BIT = 1");
        xEventGroupSetBits(wifi_event_group, WIFI_CONNECTED_BIT);
    }
}

测试网络:(可选)

我们使用http_client来测试www.baidu.com获取head,看是否已经连接上互联网了。

static bool test_internet_connection(void)
{
    esp_http_client_config_t config = {
        .url = TEST_URL,
        .timeout_ms = 5000,
        .method = HTTP_METHOD_HEAD,   // 只获取头,节省流量
    };
    esp_http_client_handle_t client = esp_http_client_init(&config);
    esp_err_t err = esp_http_client_perform(client);
    esp_http_client_cleanup(client);

    if (err == ESP_OK) {
        ESP_LOGI(TAG, "Internet connection OK");
        return true;
    } else {
        ESP_LOGW(TAG, "Internet connection FAILED (err=%d)", err);
        return false;
    }
}

9. 阿里云MQTT连接

我们使用一个头文件定义三元组,使用条件编译包含该头文件,git忽略该头文件

image-20260306002028061

未来优化可优化三元组信息来源,三元组通过wifi验证密码时候传送并写入到flash中,mqtt连接时候主动读取

使用三元组计算剩余的参数,其中计算算法可以参考阿里云的Paho-MQTT C(嵌入式版)接入示例,点击下载链接

使用esp-mqtt内置组件完成MQTT连接:

设备证书:

image-20260305005641077

MQTT 连接参数:

image-20260304001117066

查看面板得出部分MQTT连接参数与旧版不一致,目前我们仍然使用旧版的连接参数

阿里云的设备证书是三元组(一机一密):

"ProductKey": "k1kthkYpz4S",

"DeviceName": "u2VnkJdhcYH9fLDEIxbL",

"DeviceSecret": "b0282e06f0d4b571bd1ac548b7c4acee"

固定的参数:

port: 1883

YourRegionId: cn-shanghai

需要生成的参数:

clientId: {deviceName}&{productKey}|timestamp=2524608000000,_v=paho-c-1.0.0,securemode=3,signmethod=hmacsha256,lan=C|

username: {deviceName}&{productKey}

passwd: 使用 DeviceSecret作为密钥进行SHA256的计算

mqttHostUrl: {YourProductKey}.iot-as-mqtt.{YourRegionId}.aliyuncs.com

参考 Paho-MQTT C(嵌入式版)接入示例

下载链接:https://files3b.eehaiou.com/2026/03/aiot_c_demo.zip

使用aiotMqttSign签名算法函数:

image-20260306001931635

10. 主题Topic

自定义topic:

/user/xxx为自定义topic,可以自定义添加

自带get/update两个自定义topic

物理模型系统topic:(Alink JSON 格式:id+version+params+method+data)

property/post 属性上报(设备→云端)

property/set 属性设置(设备→云端)

在mqtt事件MQTT_EVENT_CONNECTED后,执行订阅set主题与get主题,并在网页端进行调试:

image-20260306001327899
image-20260306002251118

mqtt事件MQTT_EVENT_DATA触发:

image-20260306000959082

物理模型:

image-20260306004406077

PS:发送payload与接收payload中的id是用于标识消息的身份ID

在mqtt事件MQTT_EVENT_DATA进入,表示收到了订阅主题的数据到了;在事件回调执行匹配payload主题,解析json中params对象是否存在,如果params对象存在就解析ledSwitch对象是否存在;如果存在ledSwitch对象就判断类型再获取内容值。

10.1 自定义主题

image-20260306004101657

自定义主题可以随便命名,权限有订阅 / 发布 / 订阅发布(少用) 三种

自定义报文:自定义主题上行与下行的报文定义为json,使用内置组件json进行构建对象

#/user/get
{
  "type": "command",
  "payload": {
    "action": "set_led",
    "params": {
      "state": 1
    }
  }
}
#{"type":"command","payload":{"action": "set_led","params":{"state":1}}}
#/user/update/data
{
  "type": "sensor",
  "payload": {
    "temperature": 25.5,
    "humidity": 60
  }
}
#{"type":"sensor","payload":{"temperature":25.5,"humidity":60}}
# /user/update/response
{
  "type": "response",
  "payload": {
    "action": "set_led",
    "result": "success"
  }
}
#{"type":"response","payload":{"action":"set_led","result":"success"}}
// 构建JSON
cJSON_CreateObject();
cJSON_AddNumberToObject(); //增加项
cJSON_AddStringToObject();
cJSON_AddItemToObject(); //对象嵌套
char *payload = cJSON_PrintUnformatted(root); //输出字符串
cJSON_free();
cJSON_Delete();

json报文的接收解析与发送拼装:

/**
 * @brief #{"type":"command","payload":{"action": "set_led","params":{"state":1}}}
 * 
 * @param data 
 * @param len 
 */
static void process_json_custom_topic_get_command_input(esp_mqtt_client_handle_t client, const char *data, int len)
{
    cJSON *root = cJSON_Parse(data);
    if (!root) {
        ESP_LOGE(TAG, "Failed to parse custom message JSON");
        return;
    }

    cJSON *type = cJSON_GetObjectItem(root, "type");
    cJSON *payload = cJSON_GetObjectItem(root, "payload");
    if (!type || !cJSON_IsString(type) || !payload) {
        ESP_LOGW(TAG, "Invalid custom message format");
        cJSON_Delete(root);
        return;
    }

    const char *type_str = type->valuestring;

    if (strcmp(type_str, "command") == 0) {
        // 处理命令
        cJSON *action = cJSON_GetObjectItem(payload, "action");
        cJSON *params = cJSON_GetObjectItem(payload, "params");
        if (action && cJSON_IsString(action) && params) {
            const char *action_str = action->valuestring;
            if (strcmp(action_str, "set_led") == 0) {
                cJSON *state = cJSON_GetObjectItem(params, "state");
                if (state && cJSON_IsNumber(state)) {
                    //执行 命令
                    //led_set_state(state->valueint != 0);
                    led_blink_task_ctl((bool)state->valueint);
                    ESP_LOGW(TAG, "exec custom_topic_get_command() cmd");
                    // 回复响应
                    process_json_custom_topic_update_response_output(client, "set_led", "success");
                }
            }
        }
    } else {
        ESP_LOGW(TAG, "Unknown message type: %s", type_str);
    }

    cJSON_Delete(root);
}

/**
 * @brief #{"type":"sensor","payload":{"temperature":25.5,"humidity":60}}
 * 
 * @param client 
 * @param temp 
 * @param hum 
 */
static void process_json_custom_topic_update_sensor_output(esp_mqtt_client_handle_t client, float temp, float hum)
{
    char topic[128];
    snprintf(topic, sizeof(topic), "/%s/%s/user/update/data", MQTT_CRE_PRODUCT_KEY, MQTT_CRE_DEVICE_NAME);

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "type", "sensor");
    cJSON *payload = cJSON_CreateObject();
    cJSON_AddNumberToObject(payload, "temperature", temp);
    cJSON_AddNumberToObject(payload, "humidity", hum);
    cJSON_AddItemToObject(root, "payload", payload);

    char *json_str = cJSON_PrintUnformatted(root);
    esp_mqtt_client_publish(client, topic, json_str, 0, 1, 0);
    cJSON_free(json_str);
    cJSON_Delete(root);
}

/**
 * @brief #{"type":"response","payload":{"action":"set_led","result":"success"}}
 * 
 * @param client 
 * @param action 
 * @param result 
 */
static void process_json_custom_topic_update_response_output(esp_mqtt_client_handle_t client, const char *action, const char *result)
{
    char topic[128];
    snprintf(topic, sizeof(topic), "/%s/%s/user/update/response", MQTT_CRE_PRODUCT_KEY, MQTT_CRE_DEVICE_NAME);

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "type", "response");
    cJSON *payload = cJSON_CreateObject();
    cJSON_AddStringToObject(payload, "action", action);
    cJSON_AddStringToObject(payload, "result", result);
    cJSON_AddItemToObject(root, "payload", payload);

    char *json_str = cJSON_PrintUnformatted(root);
    esp_mqtt_client_publish(client, topic, json_str, 0, 1, 0);
    cJSON_free(json_str);
    cJSON_Delete(root);
}

发布自定义get主题:

image-20260306005704438

设备收到user/get解析成功,执行开灯成功:

image-20260307000745525

11. 云端消息转发

11.1 MQTT.Fx 调试

版本为MQTT.fx 1.7.1

创建一个设备mqttfx_debug ,用于调试

使用云平台提供的 MQTT 连接参数 填入

手动订阅一个系统物理模型主题 service/property/set ,云端下发set主题命令,设备显示收到:

image-20260306001128608

11.2 云产品流转:

云产品流转是一个不错的“信使”,它可以间接实现设备间通信,数据源可以是全部设备,目的Topic只能指定具体设备,不能多设备

让自定义主题update上报的报文,在云端主动接收并下发到设备。

第一步,创建一个自定义订阅权限主题get/response

创建一个消息转发规则

规则名称:update_rspn_to_get_rspn

数据源:origin_update_response,选择全部设备,数据源选择user/update/response

数据目的: method_to_topic,方法: 发布到另外一个Topic ,选择产品:esp32c3-mini

解析脚本:

var data = payload('json'); // 设备上报数据内容,json格式
var targetTopic = "/" + productKey() + "/" + deviceName() + "/user/get/response";
writeIotTopic(1002, targetTopic, data); // 流转到另一个Topic

MQTT.fx 1.7.1测试,手动发布一个主题报文并收到云端转发的主题报文:

image-20260307004415563

云端收到update主题的response消息:

image-20260307004326834

上电让所有设备订阅/user/get/response主题:

image-20260307004213842

设备收到user/get并解析成功执行关灯成功,执行发送update/response并收到get/response:

image-20260307004058473

现在任何一台设备向云端发送update/response主题的报文,设备都会收到云端的转发的get/response主题

12. 最后

目前阶段我们已经完成了使用esp-mqtt组件上云的一个基础功能,能自定义主题的订阅与发布,能控制led设备。

  1. 能通过消息转发,能验证云端是否收到自定义主题上报的报文。
  2. 报文的拼装与解析都使用json库函数完成,高效扩展性好
  3. 使用阿里云例程的签名算法
  4. 物理模型主题与自定义主题都能正常解析与上报

本工程github代码仓库(标签:v1.0):haiou1220/esp32c3-mini-mqtt

git clone -b v1.0 --single-branch https://github.com/haiou1220/esp32c3-mini-mqtt.git
#git checkout tags/v1.0

未来目标:0.使用阿里云sdk接入阿里云iot 1.使用扫描二维码完成wifi连接+非硬编码设备参数传入 2. 使用小程序替代网页控制 3.加入云端OTA功能

标签: ESP-IDF ESP32 MQTT 单片机 物联网 阿里云
最后更新:2026年3月10日

haiou

理工男极客工程师

点赞
下一篇 >

归档

  • 2026 年 3 月
  • 2026 年 2 月

分类

  • 嵌入式
  • 服务器

00:00
目录
  • ESP32连接阿里云(ESP-IDF工具链的使用)
    • 1. 前言
    • 2. 环境安装与使用
    • 3. 强大的组件
      • 3.1 创建自定义组件
    • 4. 组织项目
    • 5. 量产烧录
    • 6. 强大的分区表
    • 7.强大的事件循环
    • 8. 连接到互联网
    • 9. 阿里云MQTT连接
    • 10. 主题Topic
      • 10.1 自定义主题
    • 11. 云端消息转发
      • 11.1 MQTT.Fx 调试
      • 11.2 云产品流转:
    • 12. 最后

COPYRIGHT © 2026 海欧的博客. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang