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

1. 前言
谈到物联网那肯定会想到esp32,正因为esp32的完善的生态发展带动了物联网的普及,让设备上云已经不是遥不可及的事情了。我们借助物联网操作系统框架以及完善的开发生态,只需要少量代码就可以让设备连接互联网。那些繁重的网络设备驱动、网络协议、云平台就交给厂商,厂商提供了简单高效的接口以及一整套开发工具链,这样一来开发一款能上云的设备就简单多了。
目标是什么:掌握ESP-IDF工具链的使用,以最简单的方式,把嵌入式设备连接到云平台(阿里云)
目标芯片选型:目前确定为esp32c3,因为c3是性价比极高的芯片方案,白菜价格。当然如果有性能的要求,选择esp32s3就可以了
选择esp32有什么优势:有完善的生态,芯片自带wifi+蓝牙,一整套物联网解决方案,文档丰富
esp32开发方式的选择:esp-idf v5.5 ,代码高效体积小
2. 环境安装与使用
- 安装esp-idf工具链,CMake Ninja用于构建编译下载烧录,如果多个版本tool路径可以相同,避免重复下载
- 安装vscode,用于代码编辑
- 执行esp-idf powershell 命令窗口,已打包好环境变量,手动导出临时环境执行export.ps1
- cd 到项目工程目录
-
idf.py create-project prj #创建项目 ,会创建CMakeLists构建配置与app_main程序prj.c #修改prj文件夹为自定义项目名称 mv ./prj ./esp32c3-mini-mqtt -
cd prj idf.py set-target esp32c3 #设置芯片为esp32c3,生成sdkconfig的配置文件;生成build目录(链接配置) - 使用vscode 打开项目文件,使用图形化执行build 执行flash monitor;使用图形化会自动创建.vscode/settings.json配置文件
-
idf.py build #编译,生成bootloader partition-table prj 的bin文件 idf.py -p COM7 flash monitor #烧录+串口监视器使用事项:
- 使用
idf.py fullclean彻底清除项目的编译产物与CMake配置 (clean仅仅是清理o,bin文件) - sdkconfig不要手动编辑,要使用menuconfig或者图形界面进行编辑
- 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 文件夹

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

烧录地址查看:.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"

方法二: 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的布局;

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

自定义分区表:存放在根目录下,命名为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的通知/信号/队列 还是使用 事件循环:
- 如果涉及到系统组件的通讯,使用event loop,但开销高
- 如果不涉及到系统组件的通讯,使用 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忽略该头文件

未来优化可优化三元组信息来源,三元组通过wifi验证密码时候传送并写入到flash中,mqtt连接时候主动读取
使用三元组计算剩余的参数,其中计算算法可以参考阿里云的Paho-MQTT C(嵌入式版)接入示例,点击下载链接
使用esp-mqtt内置组件完成MQTT连接:
设备证书:

MQTT 连接参数:

查看面板得出部分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签名算法函数:

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主题,并在网页端进行调试:


mqtt事件MQTT_EVENT_DATA触发:

物理模型:

PS:发送payload与接收payload中的id是用于标识消息的身份ID
在mqtt事件MQTT_EVENT_DATA进入,表示收到了订阅主题的数据到了;在事件回调执行匹配payload主题,解析json中params对象是否存在,如果params对象存在就解析ledSwitch对象是否存在;如果存在ledSwitch对象就判断类型再获取内容值。
10.1 自定义主题

自定义主题可以随便命名,权限有订阅 / 发布 / 订阅发布(少用) 三种
自定义报文:自定义主题上行与下行的报文定义为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主题:

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

11. 云端消息转发
11.1 MQTT.Fx 调试
版本为MQTT.fx 1.7.1
创建一个设备mqttfx_debug ,用于调试
使用云平台提供的 MQTT 连接参数 填入
手动订阅一个系统物理模型主题 service/property/set ,云端下发set主题命令,设备显示收到:

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测试,手动发布一个主题报文并收到云端转发的主题报文:

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

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

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

现在任何一台设备向云端发送update/response主题的报文,设备都会收到云端的转发的get/response主题
12. 最后
目前阶段我们已经完成了使用esp-mqtt组件上云的一个基础功能,能自定义主题的订阅与发布,能控制led设备。
- 能通过消息转发,能验证云端是否收到自定义主题上报的报文。
- 报文的拼装与解析都使用json库函数完成,高效扩展性好
- 使用阿里云例程的签名算法
- 物理模型主题与自定义主题都能正常解析与上报
本工程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功能