海欧的博客

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

ESP32上传ModBus设备数据到云平台

2026年3月14日 572点热度 0人点赞 0条评论

ESP32上传ModBus设备数据到云平台

image-20260309005412389

1. 前言

最基础的使用mqtt协议连接到云平台的程序基础框架已经搭建好了,接下来我们云平台得要有数据展示,不然只有一个壳,没啥用处。展示数据我们得要写移动app或者web前端吗,其实不用。阿里云物联网平台,有公版app或者web框架可以使用,零代码就可以做成展示应用了。接下来就是数据的来源了,这里我使用了两个ModBus设备,它们在工业或者生产环境经常使用,一条总线可以挂很多设备,线路可以很长,抗干扰能力强。单片机esp32使用modbus组件采集设备数据发送到队列,mqtt任务进行10秒间隔取队列数据并上报到云端。

2. 云平台

如果你问为什么不使用阿里云物联网的app作为展示端与控制端呢

那我只能告诉你,阿里云生活物联网飞燕平台,我是申请不到使用名额,现在申请已经要求你有公司的资质才能,那我只能使用通用的阿里云物联网平台了

阿里云生活物联网平台:偏消费类,含有设计公版app应用

阿里云物联网平台:通用类,含有设计web应用与手机web应用

2.1 阿里云IOT Studio 平台

1.创建项目

2.在项目中关联产品+管理设备(阿里云物联网平台中已创建的实例中的产品与产品中的设备)

3.在项目下新建应用(web应用或者移动应用,以web应用举例)

4.添加CNAME的域名解析到web应用的地址上 (使用moving123.cn域名),登录或者匿名访问:https://iot-ali.moving123.cn

PS:阿里云物联网服务器的域名解析是禁止使用非自家的域名服务器的,所以不能使用cloudflare代理了,即使不开启CDN,也是不行的。还好我的之前已经购买过阿里域名并完成备案,可以接入阿里云的服务了。

5.开通运营账号,用于管理后台和鉴定授权访问指定页面

6.添加CNAME解析到生成的账号后台地址上(使用moving123.cn域名),登录管理员页面:https://iot-ali-admin.moving123.cn

api接口的单播/组播/广播:

单播/组播:

image-20260309000703159

广播: /broadcast/k1kthkYpz4S/${identifier} ;把 ${identifier} 替换为group01,设备端统一订阅/broadcast/k1kthkYpz4S/group01;云端发布group01的广播主题,所有订阅该组的设备都能接收到报文。payload要求是json的params对象

image-20260310004758308

Base64 在线编码解码

2.2 阿里云IOT Studio 设计应用

image-20260310005938423

2.3 阿里云web应用访问

image-20260316005428024
image-20260310005307949

3. Modbus温湿度表

3.1 产品手册介绍

image-20260312004555562
image-20260312004505028
image-20260312004629656
image-20260312005355230
image-20260312005410887
image-20260312005446737
image-20260312000206134
image-20260312000216313

3.2 使用ModbusTools + usb-rs485调试工具

下载地址:github.com/serhmarch/ModbusTools

image-20260313001417996

ps:扫描获得设备地址是1

image-20260313002449907
image-20260313003016819

3.3 使用ttl-rs485设备,使用分析仪抓取串口数据

写个简单串口程序并抓取串口数据:

image-20260314000410316
image-20260314000451360

modbus读取温度成功

image-20260314002133492

3.4 浮点数数据格式拼装

image-20260314005109890
image-20260314005119682

3.5 浮点数数据解析

我是很笨搞了4种情况全部打印

分析:工业标准modbus 是大端模式,高字节在最前, modbus先发高字节0x41

esp32 是小端,低位在最前,而且工业设备是发送最小的一个数据单位u16,我们单片机收到两个u16

设备依次发送 41 → B7 → B1 → 32 ,单片机这里把第一个u16:0xb741存储到低地址,第二u16 :0x32b1继续存入

esp32 读出byte[0-3]为: 0xb74132b1;我们需要的是正确的大端 : 0x32b1b741;所以转换格式是CDAB ,后面我们使用esp-modbus组件的特征ID对象操作时候,定义为FLOAT_CDAB ,组件就会帮我们解析不用自己解析了

4. 电能表

4.1 产品手册介绍

image-20260312000256915
image-20260312001324383
image-20260312231336489
image-20260312001352557
image-20260312000623866

4.2 寄存器地址

参考网站:正泰DDSU666 单相电能表 RS485-MODBUS RTU通讯说明

这个网站给出了电能表寄存器地址的定义,我在官网手册是找不到的,谢谢作者;

image-20260313004125574

4.3 linux modbus调试工具

电能表太远了,没法比调试,最开始是使用树莓派linux环境调试,后面学聪明了,拉了10米的485通讯线到我房间

上瑞士军刀,linux调试modbus的利器---mbpoll

#安装命令行工具mbpoll
sudo apt update && sudo apt install mbpoll -y

ls /dev/ttyUSB*

目前已知参数是:手动按键操作查询面板得出 是9600波特率 ,8bit无校验停止1位,通讯地址是1 (这个通讯地址要手动改为2,不然与温表地址冲突)

sudo mbpoll 
/dev/ttyUSB0   串口设备
-m rtu        模式:Modbus RTU
-b 9600       波特率:9600
-s 8          数据位:8位
-p none       校验位:无
-d 1          停止位:1位 
-a 1          从站地址:1
-0            启用寄存器地址0开始
-r 1          起始地址:1
-c 10         读取数量:10个
-t 3          功能码: 1:读线圈,2:读离散输入,3:读保持,4:读输入
-1            执行一次

sudo mbpoll /dev/ttyUSB0 -m rtu -b 9600 -p none -a 1 -0 -r 0 -c 10 -t 3 -1
sudo mbpoll /dev/ttyUSB0 -m rtu -b 9600 -s 8 -p none -d 1 -a 1 -0 -t 3 -r 0 -c 15
image-20260313002835067

我们知道波特率的值是3,设备地址是1,所以我们读取回来看看,结果是正确的,说明通讯数据跑通了


#波特率的寄存器地址是:12
#从寄存器地址0开始读15个

mbpoll /dev/ttyUSB0 -m rtu -b 9600 -p none -a 1 -0 -r 0 -c 15 -t 3 -1

#从寄存器地址0x2000(8192)开始读9*2=18个

mbpoll /dev/ttyUSB0 -m rtu -b 9600 -p none -a 1 -0 -r 8192 -c 18 -t 3 -1

#读累计电能消耗,4000H=>16384,读两个word
mbpoll /dev/ttyUSB0 -m rtu -b 9600 -p none -a 1 -0 -r 16384 -c 2 -t 3 -1 
image-20260313004933008
image-20260313005702637

4.4 py程序调试

使用py脚本转换字节序:

这里是64位的树莓派系统debian,不是单片机的32小端,所以仅仅看看就可以了

import sys, struct

def convert(reg1, reg2):
    print("===== 浮点数转换结果 =====")
    print(f"输入: {reg1}, {reg2}")
    print(f"ABCD: {struct.unpack('!f', struct.pack('>HH', reg1, reg2))[0]:.6f}")
    print(f"BADC: {struct.unpack('!f', struct.pack('>HH', reg2, reg1))[0]:.6f}")
    print(f"CDAB: {struct.unpack('!f', struct.pack('<HH', reg1, reg2))[0]:.6f}")
    print(f"DCBA: {struct.unpack('!f', struct.pack('<HH', reg2, reg1))[0]:.6f}")
    print("=========================")

if __name__ == "__main__":
    if len(sys.argv)!=3:
        print("用法: python float_conv.py 寄存器1 寄存器2")
        print("示例: python float_conv.py 17258 13107")
        sys.exit(1)
    convert(int(sys.argv[1]), int(sys.argv[2]))
image-20260313005305094

发现4个字节序是:ABCD, 但是它不适合我们32为小端单片机,仅仅看看就可以了

最后我们最关心的是,电能消耗,上图:

image-20260313001943542

没错就是6451度电,那是一笔大消费呀

上图,看我树莓派,让我可以无线操控ssh终端读取数据,测试的活都是让你先上的:

image-20260313002658612

最后我们得出并确认了这一套的解析方法是能读出有效数据了,上esp32

5. ESP32程序

GPIO矩阵,可以把串口1绑定到任意gpio上(串口0默认给系统烧录使用)

5.1 定义对象

定义数据对象:

enum {
    // 电能表 CID
    CID_ENERGY_HOLD_VOLTAGE_REG = 0, // 0x2000 电压寄存器 2word
    CID_ENERGY_HOLD_CURRENT_REG,     // 0x2002 电流寄存器 2word ENERGY_
    CID_ENERGY_HOLD_POWER_REG,       // 0x2004 功率寄存器 2word
    CID_ENERGY_HOLD_ENERGY_REG,      // 0x4000 能量寄存器 2word

    // SHT30 CID
    CID_SHT30_HOLD_TEMP_REG,        // 0x0000 温度寄存器 1word ,需乘以0.1转换为实际温度值
    CID_SHT30_HOLD_HUM_REG,         // 0x0001 湿度寄存器 1word ,需乘以0.1转换为实际湿度值
    CID_SHT30_HOLD_TEMP_F_REG,      // 0x0002 温度寄存器 2word
    CID_SHT30_HOLD_HUM_F_REG,       // 0x0004 湿度寄存器 2word
    CID_MAX              // 数量
};

#define ENERGY_HOLDING_REG_BASE_ADDR_VOLTAGE    0x2000 // 电能表保持寄存器起始地址
#define ENERGY_HOLDING_REG_BASE_ADDR_ENERGY     0x4000 // 电能表保持寄存器起始地址
#define SHT30_HOLDING_REG_BASE_ADDR_TEMP_REG    0x0000 // SHT30保持寄存器起始地址

//定义保持寄存器结构体
#pragma pack(push, 1) //4字节对齐
typedef struct {
    float voltage;      //     CID_VOLTAGE_REG = 0, // 0x2000 电压寄存器 2word
    float current;      //     CID_CURRENT_REG,     // 0x2002 电流寄存器 2word
    float power;        //     CID_POWER_REG,       // 0x2004 功率寄存器 2word
    float energy;          //     CID_ENERGY_REG,      // 0x4000 能量寄 2word
} energy_holding_reg_params_t;
#pragma pack(pop)

#pragma pack(push, 1)
typedef struct {
    uint16_t temp_reg;     // 0x0000 温度寄存器 1word
    uint16_t hum_reg;     // 0x0001 湿度寄存器 1word
    float temp_f_reg;     // 0x0002 温度寄存器 2word
    float hum_f_reg;     // 0x0004 湿度寄存器 2word
} sht30_holding_reg_params_t;
#pragma pack(pop)

定义特征CID对象:

// 参数描述符表
mb_parameter_descriptor_t device_parameters[] = {
    // 电能表参数描述符
    {
        CID_ENERGY_HOLD_VOLTAGE_REG, STR("Voltage"), STR("V"), ENERGY_DEVICE_ADDR, MB_PARAM_HOLDING,
        ENERGY_HOLD_REG_START(voltage) + ENERGY_HOLDING_REG_BASE_ADDR_VOLTAGE, ENERGY_HOLD_REG_SIZE(voltage),
        ENERGY_HOLD_OFFSET(voltage), PARAM_TYPE_FLOAT_CDAB, 4,
        ENERGY_OPTS(0, 1000, 0.1), PAR_PERMS_READ_WRITE_TRIGGER
    },
    {
        CID_ENERGY_HOLD_CURRENT_REG, STR("Current"), STR("A"), ENERGY_DEVICE_ADDR, MB_PARAM_HOLDING,
        ENERGY_HOLD_REG_START(current) + ENERGY_HOLDING_REG_BASE_ADDR_VOLTAGE, ENERGY_HOLD_REG_SIZE(current),
        ENERGY_HOLD_OFFSET(current), PARAM_TYPE_FLOAT_CDAB, 4,
        ENERGY_OPTS(0, 1000, 0.1), PAR_PERMS_READ_WRITE_TRIGGER
     },
    {
        CID_ENERGY_HOLD_POWER_REG, STR("Power"), STR("W"), ENERGY_DEVICE_ADDR, MB_PARAM_HOLDING,
        ENERGY_HOLD_REG_START(power) + ENERGY_HOLDING_REG_BASE_ADDR_VOLTAGE, ENERGY_HOLD_REG_SIZE(power),
        ENERGY_HOLD_OFFSET(power), PARAM_TYPE_FLOAT_CDAB, 4,
        ENERGY_OPTS(0, 1000, 0.1), PAR_PERMS_READ_WRITE_TRIGGER
    },
    {
        CID_ENERGY_HOLD_ENERGY_REG, STR("Energy"), STR("kWh"), ENERGY_DEVICE_ADDR, MB_PARAM_HOLDING,
        ENERGY_HOLDING_REG_BASE_ADDR_ENERGY, ENERGY_HOLD_REG_SIZE(energy),
        ENERGY_HOLD_OFFSET(energy), PARAM_TYPE_FLOAT_CDAB, 4,
        ENERGY_OPTS(0, 1000, 0.1), PAR_PERMS_READ_WRITE_TRIGGER
     },
    // SHT30 参数描述符
    {
        CID_SHT30_HOLD_TEMP_REG, STR("Temperature"), STR("°C"), SHT30_DEVCE_ADDR, MB_PARAM_HOLDING,
        SHT30_HOLD_REG_START(temp_reg) + SHT30_HOLDING_REG_BASE_ADDR_TEMP_REG, SHT30_HOLD_REG_SIZE(temp_reg),
        SHT30_HOLD_OFFSET(temp_reg), PARAM_TYPE_U16, 2,
        SHT30_OPTS(-40, 125, 0.1), PAR_PERMS_READ_WRITE_TRIGGER
     },
    {
        CID_SHT30_HOLD_HUM_REG, STR("Humidity"), STR("%rH"), SHT30_DEVCE_ADDR, MB_PARAM_HOLDING,
        SHT30_HOLD_REG_START(hum_reg) + SHT30_HOLDING_REG_BASE_ADDR_TEMP_REG, SHT30_HOLD_REG_SIZE(hum_reg),
        SHT30_HOLD_OFFSET(hum_reg), PARAM_TYPE_U16, 2,
        SHT30_OPTS(0, 100, 0.1), PAR_PERMS_READ_WRITE_TRIGGER
     },
    {
        CID_SHT30_HOLD_TEMP_F_REG, STR("Temperature_F"), STR("°F"), SHT30_DEVCE_ADDR, MB_PARAM_HOLDING,
        SHT30_HOLD_REG_START(temp_f_reg) + SHT30_HOLDING_REG_BASE_ADDR_TEMP_REG, SHT30_HOLD_REG_SIZE(temp_f_reg),
        SHT30_HOLD_OFFSET(temp_f_reg), PARAM_TYPE_FLOAT_CDAB, 4,
        SHT30_OPTS(-40, 125, 0.1), PAR_PERMS_READ_WRITE_TRIGGER
     },
    {
        CID_SHT30_HOLD_HUM_F_REG, STR("Humidity_F"), STR("%rH"), SHT30_DEVCE_ADDR, MB_PARAM_HOLDING,
        SHT30_HOLD_REG_START(hum_f_reg) + SHT30_HOLDING_REG_BASE_ADDR_TEMP_REG, SHT30_HOLD_REG_SIZE(hum_f_reg),
        SHT30_HOLD_OFFSET(hum_f_reg), PARAM_TYPE_FLOAT_CDAB, 4,
        SHT30_OPTS(0, 100, 0.1), PAR_PERMS_READ_WRITE_TRIGGER
     }
};

5.2 程序逻辑

1.创建描述符号,在menuconfig使能modbus组件的float数据类型扩展的编译选项

2.创建modbus 队列,把读到的数据发送到队列中,mqtt任务主动读取,读取后上报到云平台

#队列对象定义
typedef enum {
    DEVICE_ENERGY,
    DEVICE_SHT30
} mb_device_id_t;
typedef struct {
    mb_device_id_t id;  // 设备标识
    bool is_online; // 当前是否在线
    // 电能表参数
    //float voltage;
    float current;
    float power;
    float energy;
    // 温湿度参数
    //float temp;
    //float hum;
    float temp_f;
    float hum_f;
} modbus_data_msg_t;

3.增加电能表读取成功信号 与 温湿度表读取成功的是否在线的属性

4.mqtt任务10s间隔在队列读取数据, cjson库拼装上报物模型属性报文,一次性发送多个属性

5.如果获取的队列对象中的是否在线的属性是0,就上报其他传感器属性为0,这样避免设备掉线了,云端还有传感器的数据显示非零值

image-20260315004122003

上报的各种报文的payload可以参考文章:阿里云IoT物模型-属性,服务,事件通信的topic和payload详解——设备管理运维类_在物联网平台,不可以调用pub接口向物模型通信topic发送消息。-CSDN博客

5.3 commit的功能逻辑

image-20260316004944210

5.4 modbus CID框架使用

CID设计框架是表驱动模式,不是事件回调,没有回调函数

image-20260316003459174

mbc_master_get_cid_info 会使用ctx_master_handler实例句柄,在句柄里面找到你注册的CID表,匹配你传入的cid序号, 最后在cid表里面返回一个表偏移地址,这个地址就是你cid序号要找的那一份,然后修改你传入的指针的值(二级指针),让你的param_descriptor指针指向这个地址。

image-20260316004042938

上面函数是自己实现的。你拿着param_descriptor指针,找出你要的CID的数据到时候是要打算存放在什么地址上,最后返回该地址。

image-20260316004618197

mbc_master_get_parameter函数会发起总线访问时序,它会拿着你的cid序号,使用你实例化的句柄,和你要的数据存放的地址指针,和你已经注册到句柄的cid表,函数最后会返回获取到的数据类型type与数据 (存放在这个temp_data_ptr指针位置)。mbc_master_get_parameter函数有超时机制,与检验获取的数据是否匹配你的CID表的机制,有任何不符合的现象就返回错误代码。这要求你严格定义你的CID表的需求。

5.5 代码

本工程github代码仓库(版本标签:v1.1):

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

6. 最后

目前阶段完成了读取modbus传感器的数据并上报到云平台。

难点与关键点:

1.对modbus设备的浮点数的解析处理,会有不同的字节序的问题

2.数据对象的定义,要求增加是否在线的属性,在判断了掉线后发送最后一次数据对象到队列就可以了,设备不在线了就不要再发送数据到队列了

3.mqtt程序上了,mqtt任务当读取队列的时候,读取到设备A的数据时候,主动读取清空队列腾出新空间防止两个任务生产与消费频率不一致

4.理解抽象层CID的设计逻辑与API的使用,让你摆脱报文的手动组包与解析,,定义modbus抽象层特征CID时候,要求填写正确的浮点数解析的数据类型,之前我们手动测试发现的是float_cdba类型

5.多个设备连接到modbus总线的时候,设备的数据对象要求分别定义,但是特征CID要求是定义在一起,因为modbus组件框架只能允许一个modbus实例对象运行,我们可以通过条件宏让它们在语法上是可以单独的。

标签: ESP-IDF ESP32 ModBus MQTT 物联网 阿里云
最后更新:2026年3月16日

haiou

理工男极客工程师

点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

归档

  • 2026 年 3 月
  • 2026 年 2 月

分类

  • 嵌入式
  • 服务器

00:00
目录
  • ESP32上传ModBus设备数据到云平台
    • 1. 前言
    • 2. 云平台
      • 2.1 阿里云IOT Studio 平台
      • 2.2 阿里云IOT Studio 设计应用
      • 2.3 阿里云web应用访问
    • 3. Modbus温湿度表
      • 3.1 产品手册介绍
      • 3.2 使用ModbusTools + usb-rs485调试工具
      • 3.3 使用ttl-rs485设备,使用分析仪抓取串口数据
      • 3.4 浮点数数据格式拼装
      • 3.5 浮点数数据解析
    • 4. 电能表
      • 4.1 产品手册介绍
      • 4.2 寄存器地址
      • 4.3 linux modbus调试工具
      • 4.4 py程序调试
    • 5. ESP32程序
      • 5.1 定义对象
      • 5.2 程序逻辑
      • 5.3 commit的功能逻辑
      • 5.4 modbus CID框架使用
      • 5.5 代码
    • 6. 最后

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

Theme Kratos Made By Seaton Jiang