问题的引出

最近有问到怎么用C#来写modbus协议。我提炼了一部分代码,写了一个完整的测试,能简单的模拟解modbus解析过程。但这只解决了解析的问题,如何能把整个链路跑通起来?让硬件,软件,网络,采集服务器都参与进来?

我想到用 AI 辅助做一个模拟工业风扇振动监控系统,实现数据采集。先通过网页,用webapi的形式把数据上传到服务器,实现数据显示,监控报警,转速控制。保证数据没问题之后,再实现modubs地址映射,用组态和C#来进行通讯来验证。

效果预览

网页监控数据

组态软件采集数据

非常简陋的硬件

项目背景

目标:做一个风扇振动监控系统,实时采集振动数据,上传到服务器,做异常检测。

硬件选型

  • ESP32 开发板(WiFi + 足够算力)
  • MPU6050 加速度传感器(I2C 接口,便宜好用)
  • PC 散热风扇(被测对象,12v供电,5v也能用)
  • 面包板 + 杜邦线 +开发板电源模块

软件架构

ESP32(采集) → HTTP POST → Flask 服务器 → SQLite 存储
                                              ↓
                                        Web 看板(Chart.js

看起来很简单对吧,结果有几个问题没注意到,还真被卡了几下。

I2C 初始化失败

第一版代码

#include <Wire.h>
#include <Adafruit_MPU6050.h>

Adafruit_MPU6050 mpu;

#define SDA_PIN 21
#define SCL_PIN 22

void setup() {
  Serial.begin(115200);
  Wire.begin(SDA_PIN, SCL_PIN);
  
  if (!mpu.begin()) {
    Serial.println("MPU6050 初始化失败!");
    while (1) delay(10);  // 卡死在这里
  }
}

预期:串口打印”MPU6050 初始化成功” 实际:串口打印”MPU6050 初始化失败!”,然后 ESP32 的指示灯都不亮了

排查过程(踩坑记录)

问题1:Adafruit 库的”坑”

AI 告诉我,Adafruit_MPU6050 库的 begin() 函数内部会重新调用 Wire.begin()(不带参数),这会覆盖掉你之前设置的自定义引脚

正确写法

// 把已经配置好的 Wire 对象传给 mpu.begin()
if (!mpu.begin(MPU6050_I2CADDR_DEFAULT, &Wire)) {
    Serial.println("初始化失败");
}

改完之后,还是失败了,把情况给AI说,让它去解决。

问题2:I2C 地址问题

MPU6050 的地址由 AD0 引脚决定:

  • AD0 接地 → 地址 0x68
  • AD0 接 3.3V → 地址 0x69

我的模块 AD0 引脚悬空,地址不确定。

解决:把 AD0 明确接到 GND,强制地址为 0x68。

改完之后,又失败了,这时候,我有点怀疑了,是不是库的版本和硬件不匹配呢?

问题3:上拉电阻问题

I2C 总线的 SDA 和 SCL 是开漏输出,要接上拉电阻到 3.3V 才能正常工作。

虽然 GY-521 模块理论上板载有上拉电阻,但有太便宜的,会省略掉。

验证方法:用万用表量 SDA/SCL 到 3.3V 的电阻,应该在 2kΩ~10kΩ。

最终解决:绕过库函数,直接读寄存器

折腾了一阵儿,最后用直接读取 WHO_AM_I 寄存器的方式确认硬件是否在线:

#include <Wire.h>

#define SDA_PIN 21
#define SCL_PIN 22
#define MPU_ADDR 0x68

void setup() {
  Serial.begin(115200);
  Wire.begin(SDA_PIN, SCL_PIN);
  Wire.setClock(100000);
  
  // 读取 WHO_AM_I 寄存器(地址 0x75)
  Wire.beginTransmission(MPU_ADDR);
  Wire.write(0x75);
  Wire.endTransmission(false);
  Wire.requestFrom(MPU_ADDR, 1);
  
  byte whoami = Wire.read();
  Serial.print("WHO_AM_I: 0x");
  Serial.println(whoami, HEX);  // 应该返回 0x68
}

如果返回 0x68,说明硬件在线,问题出在库函数。 如果返回 0xFF 或 0x00,说明 I2C 总线物理层不通。结果这里返回是正常,证明设备是上线了。

那就是mpu.begin() 在 ESP32 上可能和 Wire 对象的配合有问题,或者库版本有 bug。既然裸 I2C 能通信,那就绕过库的手动初始化,直接用 Wire 配置 MPU6050。

最终测试代码

#include <Wire.h>

#define SDA_PIN 21
#define SCL_PIN 22
#define MPU_ADDR 0x68

// MPU6050 寄存器地址
#define PWR_MGMT_1   0x6B
#define ACCEL_XOUT_H 0x3B
#define WHO_AM_I     0x75

void setup() {
  Serial.begin(115200);
  
  // 初始化 I2C
  Wire.begin(SDA_PIN, SCL_PIN);
  Wire.setClock(100000);
  
  // 唤醒 MPU6050(默认睡眠模式)
  Wire.beginTransmission(MPU_ADDR);
  Wire.write(PWR_MGMT_1);
  Wire.write(0x00);
  Wire.endTransmission();
  
  delay(100);
  
  // 验证 WHO_AM_I
  Wire.beginTransmission(MPU_ADDR);
  Wire.write(WHO_AM_I);
  Wire.endTransmission(false);
  Wire.requestFrom(MPU_ADDR, 1);
  
  byte whoami = Wire.read();
  if (whoami == 0x68) {
    Serial.println("MPU6050 初始化成功!");
  } else {
    Serial.print("WHO_AM_I 错误: 0x");
    Serial.println(whoami, HEX);
  }
}

void loop() {
  // 读取加速度数据
  Wire.beginTransmission(MPU_ADDR);
  Wire.write(ACCEL_XOUT_H);
  Wire.endTransmission(false);
  Wire.requestFrom(MPU_ADDR, 6);
  
  int16_t ax = (Wire.read() << 8) | Wire.read();
  int16_t ay = (Wire.read() << 8) | Wire.read();
  int16_t az = (Wire.read() << 8) | Wire.read();
  
  // 转换为 g(量程 ±8g,16位有符号)
  float ax_g = ax / 4096.0;
  float ay_g = ay / 4096.0;
  float az_g = az / 4096.0;
  
  Serial.printf("X:%.3f Y:%.3f Z:%.3f\n", ax_g, ay_g, az_g);
  delay(100);
}

这个代码运行没问题,硬件的事就算解决了。

经验总结

问题 原因 解决方法
I2C 初始化失败 Adafruit 库覆盖自定义引脚 传入 Wire 对象指针
地址不对 AD0 引脚悬空 明确接 GND 或 3.3V
总线不通 缺少上拉电阻 外接 4.7kΩ 上拉
库函数不工作 库版本兼容性问题 直接读寄存器

教训

  1. 不要完全依赖库函数,理解底层原理很重要
  2. I2C 扫描和 WHO_AM_I 是最基础的诊断手段
  3. 上拉电阻是 I2C 最常见的坑

当然,这跟我不是专门搞硬件有关系,只是略懂一些。但并不重要,重要的是借助AI和自己的分析,可以快速解决这些问题。

下一步预告

硬件搞定之后,接下来要实现:

  1. WiFi 连接 + HTTP 数据上报
  2. Flask 服务器 + Web 看板
  3. 风扇 PWM 控制 + 转速采集
  4. 基线标定 + 异常检测
  5. Modbus TCP 集成(对接组态软件)

敬请期待后续文章!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐