一、项目概述

1.1 项目背景与目标

在工业生产中,电机是最核心的动力设备。据统计,电机故障导致的非计划停机占工厂总停机时间的30%以上,其中轴承磨损和过热是两大主要故障源。传统的电机维护方式依赖人工巡检,存在以下痛点:

  • 人工巡检周期长,无法实时发现早期故障征兆
  • 依赖经验判断,缺乏量化数据支撑
  • 故障发现滞后,往往已经造成设备损坏或停产
  • 多台设备分散部署,巡检人力成本高

本项目设计一套工业电机状态监测与远程预警系统,通过振动传感器和温度传感器实时采集电机运行数据,STM32作为边缘采集节点通过Modbus RTU协议上报数据,ESP8266网关将数据通过MQTT协议上传至云端,配合PyQt5上位机实现远程监控、趋势分析和故障预警。

1.2 技术栈选型

技术领域 具体选型 选择理由
采集节点MCU STM32F103C8T6 成本低、ADC精度满足需求、Modbus生态成熟
振动传感器 ADXL345(三轴加速度计) I2C/SPI双接口、16g量程、13位分辨率
温度传感器 DS18B20 单总线接口、±0.5℃精度、无需校准
现场总线 Modbus RTU(RS485) 工业标准协议、抗干扰强、支持多从站
RS485收发器 MAX485 经典方案、半双工、最大通信距离1200m
网关 ESP8266 NodeMCU WiFi内置、成本低、Arduino生态丰富
云端协议 MQTT(EMQX Broker) 轻量级、适合IoT场景、支持QoS
上位机 Python 3.x + PyQt5 开发效率高、pyqtgraph实时绘图性能好
数据库 SQLite 轻量级、无需部署、适合单机监控场景

1.3 系统功能

  • 实时采集电机振动加速度(X/Y/Z三轴)和表面温度
  • STM32采集节点通过Modbus RTU协议上报数据,支持多台电机级联
  • ESP8266网关汇聚Modbus数据,通过MQTT上传至云端Broker
  • PyQt5上位机实时展示振动波形、温度趋势、设备状态
  • 多级报警机制:振动超限预警、温度过高报警、设备离线告警
  • SQLite历史数据存储,支持趋势回溯与导出

二、系统架构设计

2.1 整体架构

在这里插入图片描述

系统采用边缘采集 + 网关汇聚 + 云端监控的三层架构:

  • 感知层:STM32 + ADXL345 + DS18B20,负责数据采集与Modbus RTU从站响应
  • 传输层:ESP8266网关作为Modbus RTU主站,轮询各采集节点,通过WiFi/MQTT上传
  • 应用层:EMQX Broker + PyQt5上位机,负责数据接收、可视化、报警与存储

2.2 通信架构

在这里插入图片描述

  • 采集节点与网关之间:Modbus RTU over RS485(半双工,9600bps)
  • 网关与云端之间:MQTT over WiFi(QoS 1,保证至少一次送达)

三、硬件设计与连接

3.1 硬件清单

组件 型号 数量 说明
采集节点MCU STM32F103C8T6 1~N 每台电机一个采集节点
振动传感器 ADXL345 1/节点 贴装在电机外壳
温度传感器 DS18B20 1/节点 贴装在电机表面
RS485收发器 MAX485 1/节点+1 Modbus RTU物理层
网关 ESP8266 NodeMCU 1 Modbus主站+MQTT网关
电源 5V/3.3V 若干 现场供电

3.2 采集节点引脚分配(STM32F103C8T6)

ADXL345:    SCL  -> PB6  (I2C1_SCL)
            SDA  -> PB7  (I2C1_SDA)
            INT1 -> PA0  (外部中断,数据就绪)
            VCC  -> 3.3V

DS18B20:    DATA -> PA1  (GPIO,外接4.7K上拉)
            VCC  -> 3.3V

MAX485:     DI   -> PA9  (USART1_TX)
            RO   -> PA10 (USART1_RX)
            DE   -> PA8  (GPIO,发送使能)
            RE   -> PA8  (与DE短接)
            A/B  -> RS485总线

3.3 网关引脚分配(ESP8266 NodeMCU)

MAX485:     DI   -> GPIO1  (UART TX)
            RO   -> GPIO3  (UART RX)
            DE   -> GPIO5  (D1,发送使能)
            RE   -> GPIO5  (与DE短接)
            A/B  -> RS485总线

重要提醒:

  • MAX485的DE和RE引脚短接,高电平发送、低电平接收
  • RS485总线两端各接120欧姆终端电阻
  • ADXL345的SDO引脚接GND时I2C地址为0x53,接VCC时为0x1D
  • DS18B20的DATA引脚必须外接4.7K上拉电阻

四、代码实现详解

4.1 ADXL345振动传感器驱动

// adxl345.h
#ifndef __ADXL345_H
#define __ADXL345_H

#include "stm32f1xx_hal.h"
#include <math.h>

#define ADXL345_ADDR        (0x53 << 1)  // SDO接GND
#define ADXL345_DEVID       0x00
#define ADXL345_POWER_CTL   0x2D
#define ADXL345_DATA_FORMAT 0x31
#define ADXL345_BW_RATE     0x2C
#define ADXL345_DATAX0      0x32

typedef struct {
    float x;    // X轴加速度 (g)
    float y;    // Y轴加速度 (g)
    float z;    // Z轴加速度 (g)
    float rms;  // 合成振动有效值 (g)
} ADXL345_Data;

HAL_StatusTypeDef ADXL345_Init(I2C_HandleTypeDef *hi2c);
HAL_StatusTypeDef ADXL345_ReadData(I2C_HandleTypeDef *hi2c, ADXL345_Data *data);

#endif

// adxl345.c
#include "adxl345.h"

static HAL_StatusTypeDef ADXL345_WriteReg(I2C_HandleTypeDef *hi2c, uint8_t reg, uint8_t val) {
    uint8_t buf[2] = {reg, val};
    return HAL_I2C_Master_Transmit(hi2c, ADXL345_ADDR, buf, 2, 100);
}

static HAL_StatusTypeDef ADXL345_ReadRegs(I2C_HandleTypeDef *hi2c, uint8_t reg, uint8_t *buf, uint16_t len) {
    HAL_StatusTypeDef ret;
    ret = HAL_I2C_Master_Transmit(hi2c, ADXL345_ADDR, &reg, 1, 100);
    if (ret != HAL_OK) return ret;
    return HAL_I2C_Master_Receive(hi2c, ADXL345_ADDR, buf, len, 100);
}

HAL_StatusTypeDef ADXL345_Init(I2C_HandleTypeDef *hi2c) {
    uint8_t id = 0;
    ADXL345_ReadRegs(hi2c, ADXL345_DEVID, &id, 1);
    if (id != 0xE5) return HAL_ERROR;  // 器件ID校验失败

    // 数据格式:全分辨率模式,+-16g量程
    ADXL345_WriteReg(hi2c, ADXL345_DATA_FORMAT, 0x0B);
    // 采样率100Hz(满足工业电机50Hz基频的振动检测)
    ADXL345_WriteReg(hi2c, ADXL345_BW_RATE, 0x0A);
    // 进入测量模式
    ADXL345_WriteReg(hi2c, ADXL345_POWER_CTL, 0x08);

    return HAL_OK;
}

HAL_StatusTypeDef ADXL345_ReadData(I2C_HandleTypeDef *hi2c, ADXL345_Data *data) {
    uint8_t raw[6];
    HAL_StatusTypeDef ret = ADXL345_ReadRegs(hi2c, ADXL345_DATAX0, raw, 6);
    if (ret != HAL_OK) return ret;

    // 原始值转换(全分辨率模式下,3.9mg/LSB)
    int16_t raw_x = (int16_t)(raw[1] << 8 | raw[0]);
    int16_t raw_y = (int16_t)(raw[3] << 8 | raw[2]);
    int16_t raw_z = (int16_t)(raw[5] << 8 | raw[4]);

    data->x = raw_x * 0.0039f;
    data->y = raw_y * 0.0039f;
    data->z = raw_z * 0.0039f;

    // 计算合成振动RMS值(去除重力分量)
    float vx = data->x;
    float vy = data->y;
    float vz = data->z - 1.0f;  // Z轴减去1g重力
    data->rms = sqrtf(vx * vx + vy * vy + vz * vz);

    return HAL_OK;
}

4.2 DS18B20温度传感器驱动

// ds18b20.h
#ifndef __DS18B20_H
#define __DS18B20_H

#include "stm32f1xx_hal.h"

#define DS18B20_PORT  GPIOA
#define DS18B20_PIN   GPIO_PIN_1

HAL_StatusTypeDef DS18B20_Init(void);
float DS18B20_ReadTemp(void);

#endif

// ds18b20.c
#include "ds18b20.h"

static void DS18B20_SetOutput(void) {
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin   = DS18B20_PIN;
    GPIO_InitStruct.Mode  = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(DS18B20_PORT, &GPIO_InitStruct);
}

static void DS18B20_SetInput(void) {
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin  = DS18B20_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(DS18B20_PORT, &GPIO_InitStruct);
}

// 微秒延时(基于DWT周期计数器)
static void Delay_us(uint32_t us) {
    uint32_t start = DWT->CYCCNT;
    uint32_t ticks = us * (SystemCoreClock / 1000000);
    while ((DWT->CYCCNT - start) < ticks);
}

static uint8_t DS18B20_Reset(void) {
    DS18B20_SetOutput();
    HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET);
    Delay_us(480);  // 拉低至少480us
    HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET);
    DS18B20_SetInput();
    Delay_us(60);   // 等待60us后检测存在脉冲

    uint8_t presence = HAL_GPIO_ReadPin(DS18B20_PORT, DS18B20_PIN);
    Delay_us(420);
    return (presence == GPIO_PIN_RESET) ? HAL_OK : HAL_ERROR;
}

static void DS18B20_WriteByte(uint8_t byte) {
    DS18B20_SetOutput();
    for (int i = 0; i < 8; i++) {
        HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET);
        Delay_us(2);
        if (byte & 0x01) {
            HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET);
        }
        Delay_us(60);
        HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET);
        Delay_us(2);
        byte >>= 1;
    }
}

static uint8_t DS18B20_ReadByte(void) {
    uint8_t byte = 0;
    for (int i = 0; i < 8; i++) {
        DS18B20_SetOutput();
        HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET);
        Delay_us(2);
        HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET);
        DS18B20_SetInput();
        Delay_us(12);
        if (HAL_GPIO_ReadPin(DS18B20_PORT, DS18B20_PIN)) {
            byte |= (1 << i);
        }
        Delay_us(50);
    }
    return byte;
}

HAL_StatusTypeDef DS18B20_Init(void) {
    // 启用DWT周期计数器
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    return DS18B20_Reset() == HAL_OK ? HAL_OK : HAL_ERROR;
}

float DS18B20_ReadTemp(void) {
    if (DS18B20_Reset() != HAL_OK) return -999.0f;

    DS18B20_WriteByte(0xCC);  // 跳过ROM(单设备)
    DS18B20_WriteByte(0x44);  // 启动温度转换
    HAL_Delay(750);           // 12位精度需要750ms

    if (DS18B20_Reset() != HAL_OK) return -999.0f;

    DS18B20_WriteByte(0xCC);
    DS18B20_WriteByte(0xBE);  // 读取暂存器

    uint8_t lsb = DS18B20_ReadByte();
    uint8_t msb = DS18B20_ReadByte();

    int16_t raw = (msb << 8) | lsb;
    return raw * 0.0625f;  // 12位精度:0.0625℃/LSB
}

4.3 Modbus RTU从站实现

// modbus_slave.h
#ifndef __MODBUS_SLAVE_H
#define __MODBUS_SLAVE_H

#include "stm32f1xx_hal.h"

#define MODBUS_SLAVE_ADDR   0x01  // 从站地址(每台电机不同)
#define MODBUS_BAUD         9600

// 保持寄存器地址映射
// 0x0000: 振动X轴 (x100, int16)
// 0x0001: 振动Y轴 (x100, int16)
// 0x0002: 振动Z轴 (x100, int16)
// 0x0003: 振动RMS  (x100, uint16)
// 0x0004: 温度     (x10,  int16)
// 0x0005: 设备状态  (0=正常, 1=预警, 2=报警, 3=严重故障)
#define REG_VIBRATION_X     0x0000
#define REG_VIBRATION_Y     0x0001
#define REG_VIBRATION_Z     0x0002
#define REG_VIBRATION_RMS   0x0003
#define REG_TEMPERATURE     0x0004
#define REG_DEVICE_STATUS   0x0005
#define REG_COUNT           6

void Modbus_Init(UART_HandleTypeDef *huart);
void Modbus_Poll(void);
void Modbus_UpdateRegisters(float vib_x, float vib_y, float vib_z, float vib_rms, float temp);

#endif

// modbus_slave.c
#include "modbus_slave.h"
#include <string.h>

static UART_HandleTypeDef *modbus_uart;
static uint16_t holding_regs[REG_COUNT] = {0};
static uint8_t rx_buf[256];
static uint8_t tx_buf[256];

// RS485方向控制
#define RS485_DE_PORT   GPIOA
#define RS485_DE_PIN    GPIO_PIN_8
#define RS485_TX_MODE() HAL_GPIO_WritePin(RS485_DE_PORT, RS485_DE_PIN, GPIO_PIN_SET)
#define RS485_RX_MODE() HAL_GPIO_WritePin(RS485_DE_PORT, RS485_DE_PIN, GPIO_PIN_RESET)

// Modbus CRC16标准校验
static uint16_t Modbus_CRC16(uint8_t *buf, uint16_t len) {
    uint16_t crc = 0xFFFF;
    for (uint16_t i = 0; i < len; i++) {
        crc ^= buf[i];
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc >>= 1;
                crc ^= 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }
    return crc;
}

void Modbus_Init(UART_HandleTypeDef *huart) {
    modbus_uart = huart;

    // 配置RS485方向控制引脚
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin   = RS485_DE_PIN;
    GPIO_InitStruct.Mode  = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(RS485_DE_PORT, &GPIO_InitStruct);

    RS485_RX_MODE();  // 默认接收模式
    memset(holding_regs, 0, sizeof(holding_regs));
}

void Modbus_UpdateRegisters(float vib_x, float vib_y, float vib_z, float vib_rms, float temp) {
    // 浮点转定点存储(振动x100,温度x10)
    holding_regs[REG_VIBRATION_X]   = (int16_t)(vib_x * 100);
    holding_regs[REG_VIBRATION_Y]   = (int16_t)(vib_y * 100);
    holding_regs[REG_VIBRATION_Z]   = (int16_t)(vib_z * 100);
    holding_regs[REG_VIBRATION_RMS] = (uint16_t)(vib_rms * 100);
    holding_regs[REG_TEMPERATURE]   = (int16_t)(temp * 10);

    // 多级状态判断
    uint16_t status = 0;  // 默认正常
    if (vib_rms > 4.5f)       status = 3;  // 严重故障:振动>4.5g
    else if (vib_rms > 2.8f)  status = 1;  // 振动预警:>2.8g
    if (temp > 85.0f)         status = (status < 2) ? 2 : 3;  // 温度报警:>85℃
    else if (temp > 70.0f && status == 0) status = 1;  // 温度预警:>70℃
    holding_regs[REG_DEVICE_STATUS] = status;
}

// 处理功能码0x03:读保持寄存器
static void Modbus_HandleReadHolding(uint8_t *frame, uint16_t len) {
    uint16_t start_addr = (frame[2] << 8) | frame[3];
    uint16_t reg_count  = (frame[4] << 8) | frame[5];

    if (start_addr + reg_count > REG_COUNT) return;

    uint8_t resp_len = 0;
    tx_buf[resp_len++] = MODBUS_SLAVE_ADDR;
    tx_buf[resp_len++] = 0x03;
    tx_buf[resp_len++] = reg_count * 2;

    for (uint16_t i = 0; i < reg_count; i++) {
        tx_buf[resp_len++] = holding_regs[start_addr + i] >> 8;
        tx_buf[resp_len++] = holding_regs[start_addr + i] & 0xFF;
    }

    uint16_t crc = Modbus_CRC16(tx_buf, resp_len);
    tx_buf[resp_len++] = crc & 0xFF;
    tx_buf[resp_len++] = crc >> 8;

    RS485_TX_MODE();
    HAL_UART_Transmit(modbus_uart, tx_buf, resp_len, 100);
    RS485_RX_MODE();
}

void Modbus_Poll(void) {
    RS485_RX_MODE();
    if (HAL_UART_Receive(modbus_uart, rx_buf, 8, 100) != HAL_OK) return;

    // 校验从站地址
    if (rx_buf[0] != MODBUS_SLAVE_ADDR) return;

    // CRC校验
    uint16_t rx_crc = (rx_buf[7] << 8) | rx_buf[6];
    uint16_t calc_crc = Modbus_CRC16(rx_buf, 6);
    if (rx_crc != calc_crc) return;

    // 功能码分发
    switch (rx_buf[1]) {
        case 0x03: Modbus_HandleReadHolding(rx_buf, 8); break;
        default: break;
    }
}

4.4 STM32采集节点主程序

// main.c — STM32采集节点
#include "stm32f1xx_hal.h"
#include "adxl345.h"
#include "ds18b20.h"
#include "modbus_slave.h"

extern I2C_HandleTypeDef hi2c1;
extern UART_HandleTypeDef huart1;

ADXL345_Data g_vib_data;
float g_temperature = 0.0f;

int main(void) {
    HAL_Init();
    SystemClock_Config();

    MX_GPIO_Init();
    MX_I2C1_Init();
    MX_USART1_UART_Init();  // RS485 Modbus

    // 启用DWT周期计数器(微秒延时用)
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

    // 初始化传感器和Modbus
    ADXL345_Init(&hi2c1);
    DS18B20_Init();
    Modbus_Init(&huart1);

    uint32_t last_sample = 0;

    while (1) {
        // 处理Modbus请求(优先级最高)
        Modbus_Poll();

        // 每500ms采集一次传感器数据
        if (HAL_GetTick() - last_sample >= 500) {
            last_sample = HAL_GetTick();

            ADXL345_ReadData(&hi2c1, &g_vib_data);
            g_temperature = DS18B20_ReadTemp();

            // 更新Modbus寄存器
            Modbus_UpdateRegisters(
                g_vib_data.x, g_vib_data.y, g_vib_data.z,
                g_vib_data.rms, g_temperature
            );
        }
    }
}

4.5 ESP8266网关固件

// gateway.ino — ESP8266 Modbus RTU主站 + MQTT网关
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <SoftwareSerial.h>

const char* WIFI_SSID = "YourSSID";
const char* WIFI_PASS = "YourPassword";
const char* MQTT_SERVER = "broker.emqx.io";
const int   MQTT_PORT   = 1883;
const char* MQTT_TOPIC  = "industrial/motor/data";
const char* MQTT_ALARM  = "industrial/motor/alarm";

#define RS485_DE_PIN  5  // GPIO5 (D1)
SoftwareSerial rs485Serial(13, 15);  // RX=D7, TX=D8

WiFiClient espClient;
PubSubClient mqtt(espClient);

#define POLL_INTERVAL  2000
#define MAX_SLAVES     3

struct SlaveConfig {
    uint8_t addr;
    const char* name;
    bool online;
    unsigned long lastSeen;
};

SlaveConfig slaves[MAX_SLAVES] = {
    {0x01, "motor_1", false, 0},
    {0x02, "motor_2", false, 0},
    {0x03, "motor_3", false, 0}
};

uint16_t modbusCRC16(uint8_t *buf, uint16_t len) {
    uint16_t crc = 0xFFFF;
    for (uint16_t i = 0; i < len; i++) {
        crc ^= buf[i];
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x0001) { crc >>= 1; crc ^= 0xA001; }
            else { crc >>= 1; }
        }
    }
    return crc;
}

// Modbus主站:读保持寄存器
bool modbusReadHolding(uint8_t slaveAddr, uint16_t startReg, uint16_t regCount, uint16_t *result) {
    uint8_t request[8];
    request[0] = slaveAddr;
    request[1] = 0x03;
    request[2] = startReg >> 8;
    request[3] = startReg & 0xFF;
    request[4] = regCount >> 8;
    request[5] = regCount & 0xFF;
    uint16_t crc = modbusCRC16(request, 6);
    request[6] = crc & 0xFF;
    request[7] = crc >> 8;

    // 发送请求
    digitalWrite(RS485_DE_PIN, HIGH);
    delay(1);
    rs485Serial.write(request, 8);
    rs485Serial.flush();
    delay(1);
    digitalWrite(RS485_DE_PIN, LOW);

    // 等待响应
    unsigned long start = millis();
    uint8_t expected = 3 + regCount * 2 + 2;
    uint8_t response[64];
    uint8_t idx = 0;

    while (millis() - start < 200 && idx < expected) {
        if (rs485Serial.available()) {
            response[idx++] = rs485Serial.read();
        }
    }

    if (idx < expected) return false;

    // CRC校验
    uint16_t rxCrc = (response[idx-1] << 8) | response[idx-2];
    if (rxCrc != modbusCRC16(response, idx - 2)) return false;

    // 解析寄存器数据
    for (uint16_t i = 0; i < regCount; i++) {
        result[i] = (response[3 + i*2] << 8) | response[3 + i*2 + 1];
    }
    return true;
}

void pollAndPublish() {
    for (int i = 0; i < MAX_SLAVES; i++) {
        uint16_t regs[6] = {0};
        bool ok = modbusReadHolding(slaves[i].addr, 0x0000, 6, regs);

        if (ok) {
            slaves[i].online = true;
            slaves[i].lastSeen = millis();

            float vib_x   = (int16_t)regs[0] / 100.0f;
            float vib_y   = (int16_t)regs[1] / 100.0f;
            float vib_z   = (int16_t)regs[2] / 100.0f;
            float vib_rms = regs[3] / 100.0f;
            float temp    = (int16_t)regs[4] / 10.0f;
            uint16_t status = regs[5];

            // 构建JSON并发布
            JsonDocument doc;
            doc["device"]  = slaves[i].name;
            doc["vib_x"]   = round(vib_x * 100) / 100.0;
            doc["vib_y"]   = round(vib_y * 100) / 100.0;
            doc["vib_z"]   = round(vib_z * 100) / 100.0;
            doc["vib_rms"] = round(vib_rms * 100) / 100.0;
            doc["temp"]    = round(temp * 10) / 10.0;
            doc["status"]  = status;

            char payload[256];
            serializeJson(doc, payload);
            mqtt.publish(MQTT_TOPIC, payload);

            // 报警推送
            if (status >= 2) {
                JsonDocument alarmDoc;
                alarmDoc["device"] = slaves[i].name;
                alarmDoc["level"]  = (status == 3) ? "critical" : "warning";
                alarmDoc["vib_rms"] = vib_rms;
                alarmDoc["temp"]    = temp;
                char alarmPayload[200];
                serializeJson(alarmDoc, alarmPayload);
                mqtt.publish(MQTT_ALARM, alarmPayload);
            }
        } else {
            // 设备离线检测
            if (slaves[i].online && millis() - slaves[i].lastSeen > 10000) {
                slaves[i].online = false;
                JsonDocument offDoc;
                offDoc["device"] = slaves[i].name;
                offDoc["level"]  = "offline";
                char offPayload[128];
                serializeJson(offDoc, offPayload);
                mqtt.publish(MQTT_ALARM, offPayload);
            }
        }
        delay(50);
    }
}

void connectMQTT() {
    while (!mqtt.connected()) {
        String clientId = "motor_gw_" + String(random(0xffff), HEX);
        if (mqtt.connect(clientId.c_str())) {
            Serial.println("[MQTT] Connected");
        } else {
            delay(3000);
        }
    }
}

void setup() {
    Serial.begin(115200);
    pinMode(RS485_DE_PIN, OUTPUT);
    digitalWrite(RS485_DE_PIN, LOW);
    rs485Serial.begin(9600);

    WiFi.mode(WIFI_STA);
    WiFi.begin(WIFI_SSID, WIFI_PASS);
    while (WiFi.status() != WL_CONNECTED) { delay(500); }
    Serial.printf("[WiFi] IP: %s\n", WiFi.localIP().toString().c_str());

    mqtt.setServer(MQTT_SERVER, MQTT_PORT);
    mqtt.setBufferSize(512);
    connectMQTT();
}

void loop() {
    if (!mqtt.connected()) connectMQTT();
    mqtt.loop();

    static unsigned long lastPoll = 0;
    if (millis() - lastPoll >= POLL_INTERVAL) {
        lastPoll = millis();
        pollAndPublish();
    }
}

4.6 PyQt5上位机实现

MQTT客户端与数据存储
# mqtt_client.py
import json
import sqlite3
import threading
from datetime import datetime
import paho.mqtt.client as mqtt
from PyQt5.QtCore import QObject, pyqtSignal

class MotorMQTTClient(QObject):
    data_received = pyqtSignal(dict)
    alarm_received = pyqtSignal(dict)
    connection_changed = pyqtSignal(bool)

    def __init__(self, broker='broker.emqx.io', port=1883):
        super().__init__()
        self.broker = broker
        self.port = port

        self.client = mqtt.Client(client_id=f'motor_monitor_{id(self)}')
        self.client.on_connect = self._on_connect
        self.client.on_message = self._on_message
        self.client.on_disconnect = self._on_disconnect

        self.db = sqlite3.connect('motor_data.db', check_same_thread=False)
        self.db_lock = threading.Lock()
        self._init_db()

    def _init_db(self):
        cursor = self.db.cursor()
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS motor_data (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                device TEXT,
                vib_x REAL, vib_y REAL, vib_z REAL, vib_rms REAL,
                temperature REAL, status INTEGER,
                timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
            )
        ''')
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS alarm_log (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                device TEXT, level TEXT,
                vib_rms REAL, temperature REAL,
                timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
            )
        ''')
        self.db.commit()

    def _on_connect(self, client, userdata, flags, rc):
        if rc == 0:
            client.subscribe('industrial/motor/data')
            client.subscribe('industrial/motor/alarm')
            self.connection_changed.emit(True)

    def _on_disconnect(self, client, userdata, rc):
        self.connection_changed.emit(False)

    def _on_message(self, client, userdata, msg):
        try:
            data = json.loads(msg.payload.decode())
            data['recv_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            if msg.topic == 'industrial/motor/data':
                self._save_data(data)
                self.data_received.emit(data)
            elif msg.topic == 'industrial/motor/alarm':
                self._save_alarm(data)
                self.alarm_received.emit(data)
        except Exception as e:
            print(f'Message parse error: {e}')

    def _save_data(self, data):
        with self.db_lock:
            self.db.execute(
                'INSERT INTO motor_data (device,vib_x,vib_y,vib_z,vib_rms,temperature,status) VALUES (?,?,?,?,?,?,?)',
                (data.get('device'), data.get('vib_x',0), data.get('vib_y',0),
                 data.get('vib_z',0), data.get('vib_rms',0), data.get('temp',0), data.get('status',0)))
            self.db.commit()

    def _save_alarm(self, data):
        with self.db_lock:
            self.db.execute(
                'INSERT INTO alarm_log (device,level,vib_rms,temperature) VALUES (?,?,?,?)',
                (data.get('device'), data.get('level'), data.get('vib_rms',0), data.get('temp',0)))
            self.db.commit()

    def start(self):
        self.client.connect_async(self.broker, self.port)
        self.client.loop_start()

    def stop(self):
        self.client.loop_stop()
        self.client.disconnect()
        self.db.close()
监控主界面
# main_window.py
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import pyqtgraph as pg
from mqtt_client import MotorMQTTClient

class MotorMonitor(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('工业电机状态监测系统')
        self.setGeometry(100, 100, 1400, 900)
        self.setStyleSheet('background-color: #1a1a2e; color: #e0e0e0;')

        self.history = {}
        self.max_points = 300
        self._init_ui()
        self._init_mqtt()

    def _init_ui(self):
        central = QWidget()
        self.setCentralWidget(central)
        main_layout = QVBoxLayout(central)

        # 顶部标题栏
        header = QHBoxLayout()
        title = QLabel('工业电机状态监测系统')
        title.setStyleSheet('font-size: 20px; font-weight: bold; color: #00d2ff; padding: 8px;')
        header.addWidget(title)
        self.conn_label = QLabel('MQTT: 断开')
        self.conn_label.setStyleSheet('font-size: 13px; color: #ff6b6b; padding: 8px;')
        header.addStretch()
        header.addWidget(self.conn_label)
        main_layout.addLayout(header)

        content = QHBoxLayout()
        main_layout.addLayout(content)

        # 左侧:设备状态卡片
        left_panel = QVBoxLayout()
        content.addLayout(left_panel, 1)
        self.device_cards = {}
        for dev_id, dev_name in [('motor_1','1号电机'), ('motor_2','2号电机'), ('motor_3','3号电机')]:
            card = self._create_device_card(dev_id, dev_name)
            left_panel.addWidget(card['widget'])
            self.device_cards[dev_id] = card
        left_panel.addStretch()

        # 右侧:实时曲线
        right_panel = QVBoxLayout()
        content.addLayout(right_panel, 3)

        # 振动RMS趋势
        self.vib_plot = pg.PlotWidget(title='振动RMS趋势 (g)')
        self.vib_plot.setBackground('#16213e')
        self.vib_plot.showGrid(x=True, y=True, alpha=0.3)
        self.vib_plot.addLegend()
        self.vib_curves = {}
        colors = {'motor_1': '#00d2ff', 'motor_2': '#ff6b6b', 'motor_3': '#ffd93d'}
        for dev_id, color in colors.items():
            self.vib_curves[dev_id] = self.vib_plot.plot(pen=pg.mkPen(color, width=2), name=dev_id)
        self.vib_plot.addLine(y=2.8, pen=pg.mkPen('#ffd93d', width=1, style=Qt.DashLine))
        self.vib_plot.addLine(y=4.5, pen=pg.mkPen('#ff6b6b', width=1, style=Qt.DashLine))
        right_panel.addWidget(self.vib_plot)

        # 温度趋势
        self.temp_plot = pg.PlotWidget(title='温度趋势 (℃)')
        self.temp_plot.setBackground('#16213e')
        self.temp_plot.showGrid(x=True, y=True, alpha=0.3)
        self.temp_plot.addLegend()
        self.temp_curves = {}
        for dev_id, color in colors.items():
            self.temp_curves[dev_id] = self.temp_plot.plot(pen=pg.mkPen(color, width=2), name=dev_id)
        self.temp_plot.addLine(y=70, pen=pg.mkPen('#ffd93d', width=1, style=Qt.DashLine))
        self.temp_plot.addLine(y=85, pen=pg.mkPen('#ff6b6b', width=1, style=Qt.DashLine))
        right_panel.addWidget(self.temp_plot)

        # 底部:报警日志
        self.alarm_list = QListWidget()
        self.alarm_list.setMaximumHeight(120)
        self.alarm_list.setStyleSheet(
            'QListWidget { background-color: #16213e; border: 1px solid #333; font-size: 12px; }'
        )
        main_layout.addWidget(QLabel('报警日志:'))
        main_layout.addWidget(self.alarm_list)

    def _create_device_card(self, dev_id, dev_name):
        widget = QFrame()
        widget.setStyleSheet('''
            QFrame { background-color: #16213e; border: 1px solid #333;
                     border-radius: 8px; padding: 10px; margin: 4px; }
        ''')
        layout = QVBoxLayout(widget)
        name_label = QLabel(dev_name)
        name_label.setStyleSheet('font-size: 15px; font-weight: bold; color: #00d2ff;')
        status_label = QLabel('离线')
        status_label.setStyleSheet('font-size: 12px; color: #888;')
        vib_label = QLabel('振动: -- g')
        vib_label.setStyleSheet('font-size: 18px; font-weight: bold;')
        temp_label = QLabel('温度: -- ℃')
        temp_label.setStyleSheet('font-size: 18px; font-weight: bold;')
        layout.addWidget(name_label)
        layout.addWidget(status_label)
        layout.addWidget(vib_label)
        layout.addWidget(temp_label)
        return {'widget': widget, 'status_label': status_label,
                'vib_label': vib_label, 'temp_label': temp_label}

    def _init_mqtt(self):
        self.mqtt_client = MotorMQTTClient()
        self.mqtt_client.data_received.connect(self.on_data)
        self.mqtt_client.alarm_received.connect(self.on_alarm)
        self.mqtt_client.connection_changed.connect(self.on_connection)
        self.mqtt_client.start()

    @pyqtSlot(bool)
    def on_connection(self, connected):
        if connected:
            self.conn_label.setText('MQTT: 已连接')
            self.conn_label.setStyleSheet('font-size: 13px; color: #00d2ff; padding: 8px;')
        else:
            self.conn_label.setText('MQTT: 断开')
            self.conn_label.setStyleSheet('font-size: 13px; color: #ff6b6b; padding: 8px;')

    @pyqtSlot(dict)
    def on_data(self, data):
        dev = data.get('device', '')
        if dev not in self.device_cards:
            return
        card = self.device_cards[dev]
        vib_rms = data.get('vib_rms', 0)
        temp = data.get('temp', 0)
        status = data.get('status', 0)

        card['vib_label'].setText(f'振动: {vib_rms:.2f} g')
        card['temp_label'].setText(f'温度: {temp:.1f} ℃')

        status_map = {0: ('运行正常', '#00d2ff'), 1: ('预警', '#ffd93d'),
                      2: ('报警', '#ff6b6b'), 3: ('严重故障', '#ff0000')}
        text, color = status_map.get(status, ('未知', '#888'))
        card['status_label'].setText(text)
        card['status_label'].setStyleSheet(f'font-size: 12px; color: {color};')

        # 更新历史曲线
        if dev not in self.history:
            self.history[dev] = {'vib_rms': [], 'temp': []}
        self.history[dev]['vib_rms'].append(vib_rms)
        self.history[dev]['temp'].append(temp)
        for k in self.history[dev]:
            if len(self.history[dev][k]) > self.max_points:
                self.history[dev][k] = self.history[dev][k][-self.max_points:]

        if dev in self.vib_curves:
            self.vib_curves[dev].setData(self.history[dev]['vib_rms'])
        if dev in self.temp_curves:
            self.temp_curves[dev].setData(self.history[dev]['temp'])

    @pyqtSlot(dict)
    def on_alarm(self, data):
        dev = data.get('device', '')
        level = data.get('level', '')
        ts = data.get('recv_time', '')
        level_map = {'warning': ('预警','#ffd93d'), 'critical': ('严重','#ff0000'), 'offline': ('离线','#888')}
        name, color = level_map.get(level, (level, '#fff'))
        msg = f'[{ts}] {dev} - {name}'
        if 'vib_rms' in data: msg += f' | 振动:{data["vib_rms"]:.2f}g'
        if 'temp' in data: msg += f' | 温度:{data["temp"]:.1f}℃'
        item = QListWidgetItem(msg)
        item.setForeground(QColor(color))
        self.alarm_list.insertItem(0, item)
        while self.alarm_list.count() > 100:
            self.alarm_list.takeItem(self.alarm_list.count() - 1)

    def closeEvent(self, event):
        self.mqtt_client.stop()
        super().closeEvent(event)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MotorMonitor()
    window.show()
    sys.exit(app.exec_())

五、调试与常见问题

5.1 Modbus通信调试

调试RS485通信时,建议先用USB转RS485模块配合Modbus Poll软件验证从站响应:

  1. 设置串口参数:9600, 8N1
  2. 功能码03,起始地址0x0000,寄存器数量6
  3. 正常响应应返回12字节数据(6个寄存器 x 2字节)

常见问题:

  • 无响应:检查A/B线是否接反、DE/RE引脚电平是否正确、从站地址是否匹配
  • CRC错误:确认双方CRC算法一致(低字节在前)、波特率一致
  • 数据乱码:RS485总线两端是否接了120欧终端电阻、线缆是否过长(超过100m建议降低波特率)

5.2 振动传感器安装要点

  • ADXL345必须牢固贴装在电机外壳上,建议使用环氧胶或螺丝固定
  • 传感器轴向与电机轴对齐,Z轴垂直于安装面
  • 避免安装在电机散热风扇附近(气流干扰)
  • I2C线缆长度不宜超过50cm,超过建议改用SPI接口

5.3 温度传感器注意事项

  • DS18B20探头需紧贴电机外壳,用导热硅脂填充间隙
  • 单总线DATA引脚必须接4.7K上拉电阻
  • 转换时间750ms(12位精度),采集频率不宜超过1Hz

5.4 ESP8266网关稳定性

  • SoftwareSerial在高波特率下不稳定,RS485通信建议不超过9600bps
  • MQTT断线重连需要指数退避,避免频繁重连导致Broker封禁
  • 建议增加看门狗(ESP.wdtEnable),防止程序卡死
  • 内存不足时(heap小于10KB),主动重启清理碎片

六、项目总结与扩展

6.1 项目成果

本项目实现了一套完整的工业电机状态监测与远程预警系统:

  • 边缘采集:STM32 + ADXL345 + DS18B20,500ms采样周期,Modbus RTU标准协议上报
  • 网关汇聚:ESP8266轮询最多3台从站,MQTT上传至云端Broker
  • 远程监控:PyQt5上位机实时展示振动波形、温度趋势、设备状态
  • 智能预警:多级报警机制(预警/报警/严重故障/离线),报警日志持久化存储
  • 数据存储:SQLite本地数据库,支持历史趋势回溯

6.2 关键技术点

  • Modbus RTU协议实现:从站寄存器映射、CRC16校验、RS485半双工控制
  • 振动数据处理:三轴加速度采集、RMS合成值计算、阈值分级判断
  • MQTT物联网通信:QoS保证、断线重连、JSON数据序列化
  • 多设备管理:从站地址编址、设备在线检测、离线告警

6.3 扩展方向

  • 增加FFT频谱分析,实现轴承故障特征频率提取
  • 接入TensorFlow Lite边缘推理,实现故障模式智能识别
  • 替换为工业级振动传感器(IEPE型),提升测量精度和频率范围
  • 接入阿里云IoT/华为云IoT,实现手机端远程监控
  • 增加FreeRTOS多任务调度,分离采集任务和通信任务
  • 加入历史数据导出(CSV/Excel),方便维护人员分析

如果这篇文章对你有帮助,请点赞收藏关注,后续会持续更新更多嵌入式物联网项目实战!

有问题欢迎评论区交流,看到都会回复。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐