BLOG8
跳转至

多路继电器自动化控制系统

项目概述

本项目构建一个16路继电器自动化控制系统,具备:

  • 16路独立控制:通过2片PCF8574 I2C扩展,用4根线控制16路继电器
  • 顺序控制:按预设时序依次启动/停止设备(如生产线启动顺序)
  • 互锁保护:防止互斥设备同时运行(如正反转不能同时接通)
  • 条件触发:传感器输入触发继电器动作(如温度超限启动风扇)
  • Modbus RTU:标准工业通信协议,可接入SCADA系统

硬件清单:

器件 型号 数量
主控 STM32F103C8T6 1
I2C扩展 PCF8574 2
继电器模块 8路5V继电器板 2
RS485收发器 MAX485 1
电源 12V/2A + 5V/3A 1

背景知识

工业自动化层次结构

现代工业自动化系统采用分层架构,从底层设备到顶层管理形成完整的控制体系:

┌─────────────────────────────────────────────────────────┐
│  企业层 (Enterprise Level)                               │
│  ERP、MES、生产计划、资源管理                            │
└─────────────────────────────────────────────────────────┘
                          ↕ Ethernet/OPC UA
┌─────────────────────────────────────────────────────────┐
│  监控层 (Supervisory Level)                              │
│  SCADA、HMI、数据采集、报表生成                          │
└─────────────────────────────────────────────────────────┘
                          ↕ Modbus TCP/Profinet
┌─────────────────────────────────────────────────────────┐
│  控制层 (Control Level)                                  │
│  PLC、DCS、运动控制器、逻辑控制                          │
└─────────────────────────────────────────────────────────┘
                          ↕ Modbus RTU/CAN/Profibus
┌─────────────────────────────────────────────────────────┐
│  现场层 (Field Level)                                    │
│  传感器、执行器、继电器、变频器、阀门                    │
└─────────────────────────────────────────────────────────┘

各层职责:

  1. 现场层(Field Level):
  2. 直接与物理过程交互的设备
  3. 传感器采集温度、压力、流量等物理量
  4. 执行器(继电器、电机、阀门)执行控制命令
  5. 本项目的16路继电器模块属于此层

  6. 控制层(Control Level):

  7. 实时逻辑控制和数据处理
  8. PLC执行梯形图/结构化文本程序
  9. 运动控制器执行轨迹规划
  10. 本项目的STM32主控承担此层功能

  11. 监控层(Supervisory Level):

  12. 人机交互界面(HMI)
  13. 数据采集与监视控制(SCADA)
  14. 历史数据存储和趋势分析
  15. 报警管理和事件记录

  16. 企业层(Enterprise Level):

  17. 生产计划和调度
  18. 制造执行系统(MES)
  19. 企业资源规划(ERP)
  20. 质量管理和追溯

通信协议选择:

层级 常用协议 特点 应用场景
企业层↔监控层 OPC UA, MQTT 跨平台、安全、语义化 云端集成、远程监控
监控层↔控制层 Modbus TCP, Profinet 以太网、高速、大数据量 工厂内部网络
控制层↔现场层 Modbus RTU, CAN, Profibus 串行总线、实时性强、抗干扰 现场设备互联

本项目采用**Modbus RTU**作为控制层与现场层的通信协议,原因: - 协议简单,易于实现 - 工业标准,兼容性好 - RS485物理层,抗干扰能力强 - 支持多主多从拓扑

Modbus RTU协议详解

Modbus RTU是Modbus协议的串行传输模式,采用二进制编码和CRC校验。

帧结构

Modbus RTU帧由以下字段组成:

┌──────┬──────┬──────────┬──────────┬─────┬─────┐
│ 从站 │ 功能 │ 数据地址 │ 数据内容 │ CRC │ CRC │
│ 地址 │ 码   │ (2字节)  │ (N字节)  │ 低  │ 高  │
│ 1B   │ 1B   │          │          │ 1B  │ 1B  │
└──────┴──────┴──────────┴──────────┴─────┴─────┘
  ↑      ↑        ↑           ↑         ↑
  1-247  见下表   大端序      功能码相关  CRC-16/Modbus

字节级示例(读取从站1的线圈0~7状态):

请求帧:
  01    03    00 00    00 08    44 0C
  │     │     │        │        └─ CRC-16 (0x0C44)
  │     │     │        └─ 数量:8个线圈
  │     │     └─ 起始地址:0x0000
  │     └─ 功能码:0x03 (读保持寄存器)
  └─ 从站地址:1

响应帧:
  01    03    10    12 34 56 78 ...    XX XX
  │     │     │     │                  └─ CRC-16
  │     │     │     └─ 数据内容(16字节)
  │     │     └─ 字节数:16
  │     └─ 功能码:0x03
  └─ 从站地址:1

常用功能码

功能码 名称 操作对象 说明
0x01 Read Coils 线圈(输出) 读取1~2000个线圈状态
0x02 Read Discrete Inputs 离散输入 读取1~2000个输入状态
0x03 Read Holding Registers 保持寄存器 读取1~125个16位寄存器
0x04 Read Input Registers 输入寄存器 读取1~125个16位寄存器
0x05 Write Single Coil 线圈 写单个线圈(0xFF00=ON, 0x0000=OFF)
0x06 Write Single Register 保持寄存器 写单个16位寄存器
0x0F Write Multiple Coils 线圈 写多个线圈(1~1968个)
0x10 Write Multiple Registers 保持寄存器 写多个寄存器(1~123个)

数据模型:

Modbus定义了4种数据类型:

地址空间:
  线圈(Coils):           0x0000 ~ 0xFFFF  读写  1位  输出继电器
  离散输入(Discrete):    0x0000 ~ 0xFFFF  只读  1位  输入开关
  输入寄存器(Input Reg): 0x0000 ~ 0xFFFF  只读  16位 ADC采样值
  保持寄存器(Holding Reg):0x0000 ~ 0xFFFF  读写  16位 配置参数

CRC-16/Modbus计算

Modbus RTU使用CRC-16校验,多项式为0xA001(反向表示):

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 (int j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc = (crc >> 1) ^ 0xA001;  // 多项式
            } else {
                crc >>= 1;
            }
        }
    }
    return crc;  // 低字节在前,高字节在后
}

CRC计算示例:

数据:01 03 00 00 00 08
步骤:
  初始CRC = 0xFFFF
  处理0x01: CRC = 0xFF00 ^ 0x01 = 0xFF01 → 移位8次 → 0x0C44
  处理0x03: ...
  最终CRC = 0x0C44
  帧尾追加:44 0C(低字节在前)

RS485电气特性

RS485是Modbus RTU的物理层标准,采用差分信号传输。

差分信号原理

RS485使用一对双绞线(A+和B-)传输差分信号:

发送端:                      接收端:
  ┌────┐                      ┌────┐
  │ TX │─ A+ ─────────────── A+ ─│ RX │
  │    │                          │    │
  │    │─ B- ─────────────────── B- ─│    │
  └────┘                      └────┘
         ↑                      ↑
      差分电压              差分电压
      VA - VB              VA - VB

逻辑1(Mark):  VA - VB < -200mV  (B电压高于A)
逻辑0(Space): VA - VB > +200mV  (A电压高于B)

抗干扰原理: - 共模干扰(如电磁辐射)同时影响A和B线 - 差分接收器只关心VA-VB,共模干扰被抵消 - 双绞线绞合进一步减少电磁耦合

总线拓扑

RS485支持多点通信(最多32个节点,使用中继器可扩展到256个):

主站                从站1      从站2      从站3
┌────┐             ┌────┐    ┌────┐    ┌────┐
│ PC │             │ STM│    │ STM│    │ STM│
│    │             │ 32 │    │ 32 │    │ 32 │
└─┬──┘             └─┬──┘    └─┬──┘    └─┬──┘
  │ A+ ───────────────┼────────┼────────┼───
  │ B- ───────────────┼────────┼────────┼───
  │ GND ──────────────┼────────┼────────┼───
  │                   │        │        │
 120Ω               (无)     (无)     120Ω
 终端电阻                              终端电阻

关键参数:

参数 典型值 说明
最大传输距离 1200m 9600bps时,更高波特率需缩短距离
最大节点数 32个 标准负载,使用中继器可扩展
差分电压 ±1.5V ~ ±6V 发送端输出电压
接收灵敏度 ±200mV 接收端最小可识别电压差
共模电压范围 -7V ~ +12V 允许的地电位差
波特率 9600 ~ 115200 常用115200bps

终端电阻

总线两端必须接120Ω终端电阻,防止信号反射:

为什么需要终端电阻?
  信号在传输线末端遇到阻抗不匹配时会反射
  反射信号叠加在原信号上,导致波形失真
  120Ω匹配双绞线特性阻抗(约120Ω)

接法:
  ┌─ A+ ─┬─ 120Ω ─┬─ B- ─┐
  │      │         │      │
  └──────┴─────────┴──────┘
         总线末端节点

错误接法: - ❌ 每个节点都接终端电阻 → 总线负载过重,电压不足 - ❌ 不接终端电阻 → 信号反射,通信不稳定 - ✅ 仅在总线两端接终端电阻

MAX485芯片

MAX485是常用的RS485收发器芯片:

引脚定义:
  RO (Receiver Output):  接MCU的RX引脚
  RE (Receiver Enable):  低电平使能接收
  DE (Driver Enable):    高电平使能发送
  DI (Driver Input):     接MCU的TX引脚
  A:  差分信号正端
  B:  差分信号负端
  VCC:5V电源
  GND:地

典型连接:
  STM32_TX ──→ DI
  STM32_RX ←── RO
  STM32_GPIO ─→ DE/RE(通常短接)
  A ──→ 总线A+
  B ──→ 总线B-

半双工控制:

RS485是半双工通信,同一时刻只能发送或接收:

// 发送前使能发送
HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_SET);
HAL_UART_Transmit(&huart1, tx_buf, tx_len, 100);
HAL_Delay(1);  // 等待发送完成
HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_RESET);

// 接收时DE/RE保持低电平
HAL_UART_Receive(&huart1, rx_buf, rx_len, 1000);

PCF8574驱动

PCF8574是8位I2C I/O扩展芯片,两片级联实现16路输出:

#include "i2c.h"

// PCF8574地址(A0~A2引脚决定)
#define PCF8574_ADDR1  0x20  // 第1片:A2A1A0=000
#define PCF8574_ADDR2  0x21  // 第2片:A2A1A0=001

// 继电器状态(16位,每位对应一路,1=断开,0=吸合)
// PCF8574输出低电平时继电器吸合(低电平触发)
static uint8_t relay_state[2] = {0xFF, 0xFF};  // 初始全断开

// 写PCF8574输出
static void pcf8574_write(uint8_t addr, uint8_t data) {
    HAL_I2C_Master_Transmit(&hi2c1, addr << 1, &data, 1, 100);
}

// 设置单路继电器
void relay_set(uint8_t ch, bool on) {
    if (ch >= 16) return;
    uint8_t chip = ch / 8;
    uint8_t bit  = ch % 8;

    if (on) relay_state[chip] &= ~(1 << bit);   // 低电平吸合
    else    relay_state[chip] |=  (1 << bit);   // 高电平断开

    uint8_t addr = (chip == 0) ? PCF8574_ADDR1 : PCF8574_ADDR2;
    pcf8574_write(addr, relay_state[chip]);
}

// 获取继电器状态
bool relay_get(uint8_t ch) {
    if (ch >= 16) return false;
    uint8_t chip = ch / 8;
    uint8_t bit  = ch % 8;
    return !(relay_state[chip] & (1 << bit));  // 0=吸合=true
}

// 批量设置(16位掩码,1=吸合)
void relay_set_all(uint16_t mask) {
    relay_state[0] = ~(mask & 0xFF);
    relay_state[1] = ~((mask >> 8) & 0xFF);
    pcf8574_write(PCF8574_ADDR1, relay_state[0]);
    pcf8574_write(PCF8574_ADDR2, relay_state[1]);
}

互锁保护

防止互斥继电器同时吸合(如电机正反转):

// 互锁组定义:同一组内只能有一路吸合
typedef struct {
    uint8_t  channels[8];  // 互锁的继电器通道号
    uint8_t  count;        // 通道数量
} InterlockGroup;

#define MAX_INTERLOCK_GROUPS  4
InterlockGroup interlock_groups[MAX_INTERLOCK_GROUPS];
uint8_t        interlock_count = 0;

// 注册互锁组(如通道0和1互锁:电机正反转)
void interlock_register(uint8_t *channels, uint8_t count) {
    if (interlock_count >= MAX_INTERLOCK_GROUPS) return;
    InterlockGroup *g = &interlock_groups[interlock_count++];
    memcpy(g->channels, channels, count);
    g->count = count;
}

// 带互锁检查的继电器设置
bool relay_set_safe(uint8_t ch, bool on) {
    if (!on) {
        relay_set(ch, false);
        return true;
    }

    // 检查是否与互锁组中其他通道冲突
    for (int i = 0; i < interlock_count; i++) {
        InterlockGroup *g = &interlock_groups[i];
        bool in_group = false;
        for (int j = 0; j < g->count; j++) {
            if (g->channels[j] == ch) { in_group = true; break; }
        }
        if (!in_group) continue;

        // 先断开同组其他通道
        for (int j = 0; j < g->count; j++) {
            if (g->channels[j] != ch && relay_get(g->channels[j])) {
                relay_set(g->channels[j], false);
                HAL_Delay(100);  // 等待继电器完全断开
            }
        }
    }

    relay_set(ch, true);
    return true;
}

顺序控制

按预设时序执行一系列继电器动作:

// 顺序控制步骤
typedef struct {
    uint8_t  channel;    // 继电器通道
    bool     action;     // true=吸合,false=断开
    uint32_t delay_ms;   // 执行后等待时间
} SeqStep;

// 示例:生产线启动顺序
// 1. 启动传送带(CH0)→ 等2秒
// 2. 启动加热器(CH1)→ 等5秒
// 3. 启动压力机(CH2)→ 等1秒
// 4. 启动出料机(CH3)
SeqStep startup_seq[] = {
    {0, true,  2000},
    {1, true,  5000},
    {2, true,  1000},
    {3, true,     0},
};

SeqStep shutdown_seq[] = {
    {3, false, 1000},
    {2, false, 1000},
    {1, false, 2000},
    {0, false,    0},
};

void run_sequence(SeqStep *seq, uint8_t steps) {
    for (int i = 0; i < steps; i++) {
        relay_set_safe(seq[i].channel, seq[i].action);
        if (seq[i].delay_ms > 0) HAL_Delay(seq[i].delay_ms);
    }
}

条件触发

传感器输入触发继电器动作:

// 触发条件类型
typedef enum {
    TRIGGER_ADC_ABOVE,   // ADC值超过阈值
    TRIGGER_ADC_BELOW,   // ADC值低于阈值
    TRIGGER_GPIO_HIGH,   // GPIO变高
    TRIGGER_GPIO_LOW,    // GPIO变低
} TriggerType;

typedef struct {
    TriggerType type;
    uint8_t     input_ch;    // 输入通道(ADC或GPIO编号)
    float       threshold;   // 阈值
    uint8_t     relay_ch;    // 触发的继电器通道
    bool        relay_action;// 触发动作
    uint32_t    debounce_ms; // 防抖时间
    uint32_t    last_trigger;
} TriggerRule;

#define MAX_RULES  8
TriggerRule rules[MAX_RULES];
uint8_t     rule_count = 0;

void trigger_check_all(void) {
    uint32_t now = HAL_GetTick();

    for (int i = 0; i < rule_count; i++) {
        TriggerRule *r = &rules[i];
        if (now - r->last_trigger < r->debounce_ms) continue;

        bool triggered = false;
        float val = 0;

        switch (r->type) {
            case TRIGGER_ADC_ABOVE:
                val = adc_read(r->input_ch);
                triggered = (val > r->threshold);
                break;
            case TRIGGER_ADC_BELOW:
                val = adc_read(r->input_ch);
                triggered = (val < r->threshold);
                break;
            case TRIGGER_GPIO_HIGH:
                triggered = HAL_GPIO_ReadPin(input_ports[r->input_ch],
                                             input_pins[r->input_ch]);
                break;
            case TRIGGER_GPIO_LOW:
                triggered = !HAL_GPIO_ReadPin(input_ports[r->input_ch],
                                              input_pins[r->input_ch]);
                break;
        }

        if (triggered) {
            relay_set_safe(r->relay_ch, r->relay_action);
            r->last_trigger = now;
        }
    }
}

Modbus RTU通信

实现标准Modbus RTU从站,支持上位机读写继电器状态:

// Modbus RTU完整实现
// 支持功能码:0x01(读线圈), 0x05(写单线圈), 0x0F(写多线圈)
// 支持功能码:0x03(读保持寄存器), 0x06(写单寄存器), 0x10(写多寄存器)

#define MODBUS_ADDR  1  // 从站地址
#define MODBUS_TIMEOUT_MS  100  // 帧间隔超时

// Modbus异常码
#define MODBUS_EX_ILLEGAL_FUNCTION      0x01
#define MODBUS_EX_ILLEGAL_DATA_ADDRESS  0x02
#define MODBUS_EX_ILLEGAL_DATA_VALUE    0x03
#define MODBUS_EX_SLAVE_DEVICE_FAILURE  0x04

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 (int j = 0; j < 8; j++) {
            if (crc & 0x0001) crc = (crc >> 1) ^ 0xA001;
            else              crc >>= 1;
        }
    }
    return crc;
}

// 发送异常响应
void modbus_send_exception(uint8_t func, uint8_t ex_code,
                           uint8_t *resp, uint16_t *resp_len) {
    resp[0] = MODBUS_ADDR;
    resp[1] = func | 0x80;  // 功能码最高位置1表示异常
    resp[2] = ex_code;
    *resp_len = 3;
    uint16_t crc = modbus_crc16(resp, 3);
    resp[3] = crc & 0xFF;
    resp[4] = crc >> 8;
    *resp_len = 5;
}

void modbus_process(uint8_t *req, uint16_t req_len,
                    uint8_t *resp, uint16_t *resp_len) {
    // 校验帧长度
    if (req_len < 4) return;

    // 校验CRC
    uint16_t crc_recv = req[req_len-2] | (req[req_len-1] << 8);
    uint16_t crc_calc = modbus_crc16(req, req_len - 2);
    if (crc_recv != crc_calc) return;  // CRC错误,丢弃帧

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

    uint8_t func = req[1];
    uint16_t addr = (req[2] << 8) | req[3];

    switch (func) {
        case 0x01: {  // 读线圈状态
            uint16_t count = (req[4] << 8) | req[5];
            if (count < 1 || count > 2000 || addr + count > 16) {
                modbus_send_exception(func, MODBUS_EX_ILLEGAL_DATA_ADDRESS,
                                     resp, resp_len);
                return;
            }
            uint8_t byte_count = (count + 7) / 8;
            resp[0] = MODBUS_ADDR;
            resp[1] = 0x01;
            resp[2] = byte_count;
            memset(&resp[3], 0, byte_count);
            for (int i = 0; i < count; i++) {
                if (relay_get(addr + i))
                    resp[3 + i/8] |= (1 << (i % 8));
            }
            *resp_len = 3 + byte_count;
            break;
        }
        case 0x03: {  // 读保持寄存器
            uint16_t count = (req[4] << 8) | req[5];
            if (count < 1 || count > 125) {
                modbus_send_exception(func, MODBUS_EX_ILLEGAL_DATA_VALUE,
                                     resp, resp_len);
                return;
            }
            resp[0] = MODBUS_ADDR;
            resp[1] = 0x03;
            resp[2] = count * 2;
            for (int i = 0; i < count; i++) {
                uint16_t val = holding_register_read(addr + i);
                resp[3 + i*2] = val >> 8;
                resp[4 + i*2] = val & 0xFF;
            }
            *resp_len = 3 + count * 2;
            break;
        }
        case 0x05: {  // 写单线圈
            uint16_t val = (req[4] << 8) | req[5];
            if (val != 0xFF00 && val != 0x0000) {
                modbus_send_exception(func, MODBUS_EX_ILLEGAL_DATA_VALUE,
                                     resp, resp_len);
                return;
            }
            if (addr >= 16) {
                modbus_send_exception(func, MODBUS_EX_ILLEGAL_DATA_ADDRESS,
                                     resp, resp_len);
                return;
            }
            relay_set_safe(addr, val == 0xFF00);
            memcpy(resp, req, 6);  // 回显请求
            *resp_len = 6;
            break;
        }
        case 0x06: {  // 写单寄存器
            uint16_t val = (req[4] << 8) | req[5];
            if (!holding_register_write(addr, val)) {
                modbus_send_exception(func, MODBUS_EX_ILLEGAL_DATA_ADDRESS,
                                     resp, resp_len);
                return;
            }
            memcpy(resp, req, 6);
            *resp_len = 6;
            break;
        }
        case 0x0F: {  // 写多线圈
            uint16_t count = (req[4] << 8) | req[5];
            uint8_t byte_count = req[6];
            if (count < 1 || count > 1968 || addr + count > 16) {
                modbus_send_exception(func, MODBUS_EX_ILLEGAL_DATA_ADDRESS,
                                     resp, resp_len);
                return;
            }
            for (int i = 0; i < count; i++) {
                bool state = (req[7 + i/8] >> (i % 8)) & 0x01;
                relay_set_safe(addr + i, state);
            }
            resp[0] = MODBUS_ADDR;
            resp[1] = 0x0F;
            resp[2] = req[2];
            resp[3] = req[3];
            resp[4] = req[4];
            resp[5] = req[5];
            *resp_len = 6;
            break;
        }
        default:
            modbus_send_exception(func, MODBUS_EX_ILLEGAL_FUNCTION,
                                 resp, resp_len);
            return;
    }

    // 追加CRC
    uint16_t crc = modbus_crc16(resp, *resp_len);
    resp[(*resp_len)++] = crc & 0xFF;
    resp[(*resp_len)++] = crc >> 8;
}

// 保持寄存器读写(示例映射)
uint16_t holding_register_read(uint16_t addr) {
    switch (addr) {
        case 0x0000:  // 继电器状态掩码
            return (relay_state[1] << 8) | relay_state[0];
        case 0x0001:  // 系统状态
            return system_status;
        case 0x0002:  // ADC输入1
            return adc_read(0);
        case 0x0003:  // ADC输入2
            return adc_read(1);
        case 0x0010:  // 顺序控制状态
            return sequence_state;
        case 0x0020:  // 系统运行时间(秒)
            return HAL_GetTick() / 1000;
        default:
            return 0;
    }
}

bool holding_register_write(uint16_t addr, uint16_t val) {
    switch (addr) {
        case 0x0000:  // 批量设置继电器
            relay_set_all(val);
            return true;
        case 0x0010:  // 顺序控制命令
            if (val == 1) run_sequence(startup_seq, 4);
            else if (val == 2) run_sequence(shutdown_seq, 4);
            return true;
        default:
            return false;  // 只读或不存在的地址
    }
}

Modbus TCP网关实现

将Modbus RTU设备桥接到以太网,实现远程访问:

// 使用W5500以太网模块实现Modbus TCP网关
// Modbus TCP = MBAP头(7字节) + Modbus PDU

#include "w5500.h"

#define MODBUS_TCP_PORT  502
#define MAX_TCP_CLIENTS  4

typedef struct {
    uint16_t transaction_id;  // 事务标识符
    uint16_t protocol_id;     // 协议标识符(0=Modbus)
    uint16_t length;          // 后续字节数
    uint8_t  unit_id;         // 单元标识符(从站地址)
} MBAP_Header;

void modbus_tcp_gateway_task(void) {
    uint8_t sock = 0;
    uint8_t rx_buf[260];
    uint8_t tx_buf[260];
    uint16_t rx_len, tx_len;

    // 监听TCP 502端口
    if (getSn_SR(sock) == SOCK_CLOSED) {
        socket(sock, Sn_MR_TCP, MODBUS_TCP_PORT, 0);
        listen(sock);
    }

    if (getSn_SR(sock) == SOCK_ESTABLISHED) {
        rx_len = getSn_RX_RSR(sock);
        if (rx_len >= 7) {  // 至少有MBAP头
            recv(sock, rx_buf, rx_len);

            // 解析MBAP头
            MBAP_Header *mbap = (MBAP_Header*)rx_buf;
            mbap->transaction_id = ntohs(mbap->transaction_id);
            mbap->protocol_id = ntohs(mbap->protocol_id);
            mbap->length = ntohs(mbap->length);

            if (mbap->protocol_id != 0) {
                disconnect(sock);
                return;
            }

            // 提取Modbus PDU(去掉MBAP头)
            uint8_t *pdu = &rx_buf[7];
            uint16_t pdu_len = mbap->length - 1;

            // 转发到Modbus RTU从站
            uint8_t rtu_req[260];
            rtu_req[0] = mbap->unit_id;  // 从站地址
            memcpy(&rtu_req[1], pdu, pdu_len);
            uint16_t crc = modbus_crc16(rtu_req, pdu_len + 1);
            rtu_req[pdu_len + 1] = crc & 0xFF;
            rtu_req[pdu_len + 2] = crc >> 8;

            // 通过RS485发送
            HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_SET);
            HAL_UART_Transmit(&huart1, rtu_req, pdu_len + 3, 100);
            HAL_Delay(1);
            HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_RESET);

            // 接收RTU响应
            uint8_t rtu_resp[260];
            uint16_t rtu_resp_len = 0;
            HAL_UART_Receive(&huart1, rtu_resp, 260, 1000);
            // 实际应用中需要根据功能码计算响应长度

            // 去掉从站地址和CRC,提取PDU
            uint8_t *resp_pdu = &rtu_resp[1];
            uint16_t resp_pdu_len = rtu_resp_len - 3;

            // 构造MBAP响应
            MBAP_Header *resp_mbap = (MBAP_Header*)tx_buf;
            resp_mbap->transaction_id = htons(mbap->transaction_id);
            resp_mbap->protocol_id = 0;
            resp_mbap->length = htons(resp_pdu_len + 1);
            resp_mbap->unit_id = mbap->unit_id;
            memcpy(&tx_buf[7], resp_pdu, resp_pdu_len);
            tx_len = 7 + resp_pdu_len;

            // 发送TCP响应
            send(sock, tx_buf, tx_len);
        }
    }
}

PLC梯形图等效C代码

将PLC梯形图逻辑转换为C代码实现:

// PLC梯形图示例:启动/停止按钮控制电机
// 
//  ┌─┤ START ├─┤ /STOP ├─┤ /ALARM ├─( MOTOR )─┐
//  │                                            │
//  └─┤ MOTOR ├────────────────────────────────┘
//
// 逻辑:按下START且未按STOP且无ALARM时启动电机,电机自保持

typedef struct {
    bool START;   // 启动按钮(常开)
    bool STOP;    // 停止按钮(常闭)
    bool ALARM;   // 报警信号(常闭)
    bool MOTOR;   // 电机输出
} PLC_IO;

PLC_IO plc;

void plc_scan_cycle(void) {
    // 读取输入
    plc.START = HAL_GPIO_ReadPin(START_GPIO_Port, START_Pin);
    plc.STOP  = !HAL_GPIO_ReadPin(STOP_GPIO_Port, STOP_Pin);  // 常闭
    plc.ALARM = !HAL_GPIO_ReadPin(ALARM_GPIO_Port, ALARM_Pin);

    // 梯形图逻辑
    bool rung1 = plc.START && plc.STOP && plc.ALARM;  // 第一条支路
    bool rung2 = plc.MOTOR;                           // 第二条支路(自保持)
    plc.MOTOR = rung1 || rung2;

    // 写入输出
    relay_set(0, plc.MOTOR);
}

// 更复杂的梯形图:三台电机顺序启动
//
//  ┌─┤ START ├─┤ /M1 ├─────────────────( M1 )─┐
//  │                                            │
//  └─┤ M1 ├───────────────────────────────────┘
//
//  ┌─┤ M1 ├─┤ TON_1 ├─┤ /M2 ├───────────( M2 )─┐
//  │                                            │
//  └─┤ M2 ├───────────────────────────────────┘
//
//  ┌─┤ M2 ├─┤ TON_2 ├─┤ /M3 ├───────────( M3 )─┐
//  │                                            │
//  └─┤ M3 ├───────────────────────────────────┘

typedef struct {
    bool IN;
    bool Q;
    uint32_t PT;  // 预设时间(ms)
    uint32_t ET;  // 经过时间(ms)
    uint32_t start_time;
} TON_Timer;

void TON_update(TON_Timer *t) {
    if (t->IN) {
        if (!t->Q) {
            if (t->start_time == 0) t->start_time = HAL_GetTick();
            t->ET = HAL_GetTick() - t->start_time;
            if (t->ET >= t->PT) t->Q = true;
        }
    } else {
        t->Q = false;
        t->ET = 0;
        t->start_time = 0;
    }
}

PLC_IO plc2;
TON_Timer TON_1 = {.PT = 2000};  // 2秒延时
TON_Timer TON_2 = {.PT = 3000};  // 3秒延时

void plc_sequential_start(void) {
    // 读取输入
    plc2.START = HAL_GPIO_ReadPin(START_GPIO_Port, START_Pin);

    // M1逻辑
    bool m1_start = plc2.START && !plc2.MOTOR;
    plc2.MOTOR = m1_start || plc2.MOTOR;
    relay_set(0, plc2.MOTOR);

    // M2逻辑(M1启动2秒后)
    TON_1.IN = plc2.MOTOR;
    TON_update(&TON_1);
    bool m2_start = TON_1.Q && !plc2.M2;
    plc2.M2 = m2_start || plc2.M2;
    relay_set(1, plc2.M2);

    // M3逻辑(M2启动3秒后)
    TON_2.IN = plc2.M2;
    TON_update(&TON_2);
    bool m3_start = TON_2.Q && !plc2.M3;
    plc2.M3 = m3_start || plc2.M3;
    relay_set(2, plc2.M3);
}

SCADA集成:Node-RED仪表板

使用Node-RED创建Web HMI界面,通过Modbus TCP读写继电器:

// Node-RED流程配置(导入到Node-RED)
[
    {
        "id": "modbus_client",
        "type": "modbus-client",
        "name": "继电器控制器",
        "clienttype": "tcp",
        "bufferCommands": true,
        "stateLogEnabled": false,
        "tcpHost": "192.168.1.100",
        "tcpPort": "502",
        "tcpType": "DEFAULT",
        "serialPort": "/dev/ttyUSB0",
        "serialType": "RTU-BUFFERD",
        "serialBaudrate": "115200",
        "serialDatabits": "8",
        "serialStopbits": "1",
        "serialParity": "none",
        "serialConnectionDelay": "100",
        "unit_id": "1",
        "commandDelay": "1",
        "clientTimeout": "1000",
        "reconnectTimeout": "2000"
    },
    {
        "id": "read_coils",
        "type": "modbus-read",
        "name": "读取继电器状态",
        "topic": "",
        "showStatusActivities": false,
        "logIOActivities": false,
        "showErrors": false,
        "unitid": "1",
        "dataType": "Coil",
        "adr": "0",
        "quantity": "16",
        "rate": "1000",
        "rateUnit": "ms",
        "delayOnStart": false,
        "startDelayTime": "",
        "server": "modbus_client",
        "useIOFile": false,
        "ioFile": "",
        "useIOForPayload": false,
        "x": 200,
        "y": 100,
        "wires": [["dashboard_display"]]
    },
    {
        "id": "dashboard_display",
        "type": "ui_template",
        "group": "relay_group",
        "name": "继电器状态显示",
        "order": 1,
        "width": "12",
        "height": "8",
        "format": "<div ng-bind-html=\"msg.payload\"></div>",
        "storeOutMessages": true,
        "fwdInMessages": true,
        "templateScope": "local",
        "x": 400,
        "y": 100,
        "wires": [[]]
    },
    {
        "id": "relay_switch",
        "type": "ui_switch",
        "group": "relay_group",
        "name": "继电器1控制",
        "label": "继电器1",
        "tooltip": "",
        "order": 2,
        "width": "3",
        "height": "1",
        "passthru": true,
        "decouple": "false",
        "topic": "",
        "style": "",
        "onvalue": "true",
        "onvalueType": "bool",
        "onicon": "",
        "oncolor": "",
        "offvalue": "false",
        "offvalueType": "bool",
        "officon": "",
        "offcolor": "",
        "x": 200,
        "y": 200,
        "wires": [["write_coil"]]
    },
    {
        "id": "write_coil",
        "type": "modbus-write",
        "name": "写入继电器",
        "showStatusActivities": false,
        "showErrors": false,
        "unitid": "1",
        "dataType": "Coil",
        "adr": "0",
        "quantity": "1",
        "server": "modbus_client",
        "x": 400,
        "y": 200,
        "wires": [[]]
    }
]

Node-RED仪表板功能: - 实时显示16路继电器状态(绿色=吸合,灰色=断开) - 开关按钮控制单路继电器 - 顺序控制按钮(启动/停止生产线) - 历史数据图表(继电器动作次数、运行时间) - 报警通知(互锁冲突、通信故障)

部署步骤: 1. 安装Node-RED:npm install -g node-red 2. 安装Modbus节点:npm install node-red-contrib-modbus 3. 安装仪表板:npm install node-red-dashboard 4. 导入上述流程JSON 5. 访问 http://localhost:1880/ui 查看仪表板

## 延伸阅读

- [继电器控制基础](../beginner/04-relay-control.md) - 继电器工作原理
- [WiFi智能插座](07-smart-socket-project.md) - 单路WiFi控制
- [环境监控系统](09-environment-control.md) - 传感器联动

## 深入原理

### IEC 61131-3编程模型

IEC 61131-3是PLC编程语言的国际标准,定义了5种编程语言:

#### 1. 梯形图(Ladder Diagram, LD)

最直观的图形化语言,模拟继电器控制电路:
基本元素: ─┤ ├─ 常开触点(Normally Open) ─┤/├─ 常闭触点(Normally Closed) ─( )─ 线圈(Coil) ─(S)─ 置位线圈(Set) ─(R)─ 复位线圈(Reset) ─[TON]─ 定时器(Timer On Delay) ─[CTU]─ 计数器(Counter Up)

示例:电机星三角启动 ┌─┤ START ├─┤ /STOP ├─┤ /KM1 ├─( KM1 )─┐ 主接触器 │ │ └─┤ KM1 ├──────────────────────────────┘ 自保持

┌─┤ KM1 ├─┤ TON_1 ├─┤ /KM2 ├─( KM2 )─┐ 星形接触器 │ │ └─┤ KM2 ├─┤ /TON_1.Q ├────────────────┘

┌─┤ KM1 ├─┤ TON_1.Q ├─┤ /KM3 ├─( KM3 )─┐ 三角形接触器 │ │ └─┤ KM3 ├──────────────────────────────┘

**C语言等效实现**:

```c
typedef struct {
    bool START, STOP, KM1, KM2, KM3;
    TON_Timer TON_1;
} StarDelta_PLC;

void star_delta_logic(StarDelta_PLC *plc) {
    // 主接触器KM1
    bool km1_set = plc->START && !plc->STOP && !plc->KM1;
    plc->KM1 = km1_set || plc->KM1;

    // 星形接触器KM2(KM1启动后立即吸合)
    plc->TON_1.IN = plc->KM1;
    TON_update(&plc->TON_1);
    bool km2_set = plc->KM1 && !plc->TON_1.Q && !plc->KM2;
    plc->KM2 = km2_set || (plc->KM2 && !plc->TON_1.Q);

    // 三角形接触器KM3(延时后切换)
    bool km3_set = plc->KM1 && plc->TON_1.Q && !plc->KM3;
    plc->KM3 = km3_set || plc->KM3;

    // 输出
    relay_set(0, plc->KM1);
    relay_set(1, plc->KM2);
    relay_set(2, plc->KM3);
}

2. 功能块图(Function Block Diagram, FBD)

类似数字电路的逻辑门连接:

     START ──┬──┐
             │  │
     STOP ───┼──┤ AND ├─┬─┐
             │  │      │ │
     ALARM ──┴──┘      │ │
                       │ │
     MOTOR ────────────┘ │
                         │
                      ┌──┴──┐
                      │  OR │──── MOTOR
                      └─────┘

3. 结构化文本(Structured Text, ST)

类似Pascal的高级语言:

(* 星三角启动程序 *)
PROGRAM StarDelta
VAR
    START, STOP : BOOL;
    KM1, KM2, KM3 : BOOL;
    TON_1 : TON;
END_VAR

(* 主接触器 *)
IF START AND NOT STOP THEN
    KM1 := TRUE;
END_IF;

IF STOP THEN
    KM1 := FALSE;
    KM2 := FALSE;
    KM3 := FALSE;
END_IF;

(* 星形接触器 *)
TON_1(IN := KM1, PT := T#5S);
IF KM1 AND NOT TON_1.Q THEN
    KM2 := TRUE;
    KM3 := FALSE;
END_IF;

(* 三角形接触器 *)
IF KM1 AND TON_1.Q THEN
    KM2 := FALSE;
    KM3 := TRUE;
END_IF;

END_PROGRAM

4. 指令表(Instruction List, IL)

类似汇编语言:

LD    START
AND   STOP
ANDN  KM1
ST    KM1

LD    KM1
TON   TON_1, T#5S
ANDN  KM2
ST    KM2

5. 顺序功能图(Sequential Function Chart, SFC)

描述顺序控制流程:

    ┌───────┐
    │ INIT  │ 初始步
    └───┬───┘
        │ START=1
    ┌───▼───┐
    │ STEP1 │ 启动主接触器
    └───┬───┘
        │ KM1=1
    ┌───▼───┐
    │ STEP2 │ 星形启动
    └───┬───┘
        │ T=5s
    ┌───▼───┐
    │ STEP3 │ 切换三角形
    └───┬───┘
        │ STOP=1
    ┌───▼───┐
    │ STOP  │ 停止
    └───────┘

安全继电器原理

安全继电器(Safety Relay)用于安全关键应用,符合IEC 61508功能安全标准。

双通道冗余架构

输入1 ──┬─→ 通道A ──┬─→ 输出1
        │           │
        │  监控     │
        │  逻辑     │
        │           │
输入2 ──┴─→ 通道B ──┴─→ 输出2

监控逻辑:
  - 两个通道独立处理
  - 交叉监控对方状态
  - 任一通道故障则安全断开
  - 定期自检(脉冲测试)

实现示例:

typedef struct {
    bool input1, input2;
    bool channel_a, channel_b;
    bool output1, output2;
    uint32_t last_test_time;
    bool test_passed;
} SafetyRelay;

void safety_relay_update(SafetyRelay *sr) {
    uint32_t now = HAL_GetTick();

    // 每100ms自检一次
    if (now - sr->last_test_time > 100) {
        sr->last_test_time = now;

        // 脉冲测试:短暂断开输出,检查反馈
        relay_set(0, false);
        relay_set(1, false);
        HAL_Delay(1);

        // 检查反馈回路
        bool fb1 = HAL_GPIO_ReadPin(FEEDBACK1_GPIO_Port, FEEDBACK1_Pin);
        bool fb2 = HAL_GPIO_ReadPin(FEEDBACK2_GPIO_Port, FEEDBACK2_Pin);
        sr->test_passed = (!fb1 && !fb2);  // 应该都断开

        if (!sr->test_passed) {
            // 自检失败,进入安全状态
            sr->channel_a = false;
            sr->channel_b = false;
            return;
        }
    }

    // 双通道逻辑
    sr->channel_a = sr->input1 && sr->test_passed;
    sr->channel_b = sr->input2 && sr->test_passed;

    // 交叉监控:两个通道必须一致
    if (sr->channel_a != sr->channel_b) {
        // 通道不一致,安全断开
        sr->output1 = false;
        sr->output2 = false;
    } else {
        sr->output1 = sr->channel_a;
        sr->output2 = sr->channel_b;
    }

    relay_set(0, sr->output1);
    relay_set(1, sr->output2);
}

安全等级(SIL)

IEC 61508定义了4个安全完整性等级:

SIL等级 失效概率(每小时) 风险降低因子 应用场景
SIL 1 10⁻⁵ ~ 10⁻⁶ 10 ~ 100 轻伤风险
SIL 2 10⁻⁶ ~ 10⁻⁷ 100 ~ 1000 重伤风险
SIL 3 10⁻⁷ ~ 10⁻⁸ 1000 ~ 10000 死亡风险
SIL 4 10⁻⁸ ~ 10⁻⁹ 10000 ~ 100000 多人死亡

达到SIL 2的措施: - 双通道冗余 - 交叉监控 - 定期自检 - 故障安全设计(Fail-Safe) - 软件符合MISRA C规范

看门狗定时器(Watchdog Timer)

防止程序跑飞或死循环,确保系统可靠运行。

独立看门狗(IWDG)

STM32内置独立看门狗,由独立RC振荡器驱动:

// 初始化IWDG,超时时间1秒
void iwdg_init(void) {
    IWDG->KR = 0x5555;  // 允许写入PR和RLR
    IWDG->PR = 6;       // 预分频器:40kHz / 256 = 156Hz
    IWDG->RLR = 156;    // 重载值:156 / 156Hz = 1秒
    IWDG->KR = 0xCCCC;  // 启动看门狗
}

// 喂狗(必须在1秒内调用)
void iwdg_feed(void) {
    IWDG->KR = 0xAAAA;
}

// 主循环
int main(void) {
    HAL_Init();
    iwdg_init();

    while (1) {
        // 正常任务
        relay_control_task();
        modbus_task();
        trigger_check_all();

        // 喂狗
        iwdg_feed();

        HAL_Delay(100);
    }
}

窗口看门狗(WWDG)

窗口看门狗要求在特定时间窗口内喂狗,防止程序运行过快或过慢:

// 初始化WWDG,窗口时间50~100ms
void wwdg_init(void) {
    __HAL_RCC_WWDG_CLK_ENABLE();

    WWDG_HandleTypeDef hwwdg;
    hwwdg.Instance = WWDG;
    hwwdg.Init.Prescaler = WWDG_PRESCALER_8;  // 72MHz / 4096 / 8 = 2.2kHz
    hwwdg.Init.Window = 80;    // 窗口上限:80 / 2.2kHz = 36ms
    hwwdg.Init.Counter = 127;  // 计数器初值
    hwwdg.Init.EWIMode = WWDG_EWI_ENABLE;  // 使能早期唤醒中断

    HAL_WWDG_Init(&hwwdg);
}

// 早期唤醒中断(在超时前触发)
void WWDG_IRQHandler(void) {
    HAL_WWDG_IRQHandler(&hwwdg);
}

void HAL_WWDG_EarlyWakeupCallback(WWDG_HandleTypeDef *hwwdg) {
    // 在窗口内喂狗
    HAL_WWDG_Refresh(hwwdg);
}

软件看门狗

多任务系统中,监控各任务是否正常运行:

#define MAX_TASKS  4

typedef struct {
    const char *name;
    uint32_t timeout_ms;
    uint32_t last_feed_time;
    bool alive;
} TaskWatchdog;

TaskWatchdog task_wdg[MAX_TASKS] = {
    {"ModbusTask",  1000, 0, true},
    {"RelayTask",   500,  0, true},
    {"TriggerTask", 2000, 0, true},
    {"LogTask",     5000, 0, true},
};

// 任务喂狗
void task_feed_watchdog(uint8_t task_id) {
    if (task_id < MAX_TASKS) {
        task_wdg[task_id].last_feed_time = HAL_GetTick();
        task_wdg[task_id].alive = true;
    }
}

// 看门狗监控任务(高优先级)
void watchdog_monitor_task(void) {
    uint32_t now = HAL_GetTick();
    bool all_alive = true;

    for (int i = 0; i < MAX_TASKS; i++) {
        if (now - task_wdg[i].last_feed_time > task_wdg[i].timeout_ms) {
            task_wdg[i].alive = false;
            all_alive = false;
            printf("Task %s timeout!\n", task_wdg[i].name);
        }
    }

    if (!all_alive) {
        // 任务超时,进入安全状态
        relay_set_all(0);  // 断开所有继电器
        system_status = STATUS_ERROR;
        // 可选:触发系统复位
        // NVIC_SystemReset();
    } else {
        // 所有任务正常,喂硬件看门狗
        iwdg_feed();
    }
}

参考资料

  1. PCF8574 Datasheet - NXP Semiconductors
  2. Modbus Application Protocol Specification V1.1b3
  3. MAX485 Datasheet - Maxim Integrated
  4. 《可编程控制器原理及应用》- 廖常初
  5. 《工业控制网络》- 阳宪惠
  6. IEC 61131-3 - PLC编程语言标准
  7. W5500 Datasheet - WIZnet
  8. IEC 61508 - 功能安全标准
  9. MISRA C:2012 - 嵌入式C编程规范
  10. Modbus TCP/IP Specification - Modbus Organization

完整项目实战:工业自动化控制柜

项目需求

设计一个16路继电器自动化控制柜,用于小型生产线控制:

控制对象: - 4台传送带电机(M1~M4) - 2台加热器(H1~H2) - 2台冷却风扇(F1~F2) - 4个电磁阀(V1~V4) - 2个指示灯(L1~L2) - 2路备用(R1~R2)

功能要求: 1. 本地HMI触摸屏控制 2. 远程Modbus TCP监控 3. 顺序启动/停止(防止电流冲击) 4. 互锁保护(加热器与风扇不能同时运行) 5. 温度/压力传感器联动 6. 故障报警和日志记录 7. 手动/自动模式切换

硬件设计

系统架构

┌─────────────────────────────────────────────────────────┐
│                    控制柜面板                            │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐              │
│  │ 急停按钮 │  │ 模式选择 │  │ 指示灯   │              │
│  └──────────┘  └──────────┘  └──────────┘              │
│                                                          │
│  ┌────────────────────────────────────────────┐         │
│  │         7寸HMI触摸屏(UART/RS485)         │         │
│  └────────────────────────────────────────────┘         │
└─────────────────────────────────────────────────────────┘
                          │
                          ↓
┌─────────────────────────────────────────────────────────┐
│                    STM32F407主控板                       │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐   │
│  │ UART1   │  │ UART2   │  │ I2C1    │  │ SPI1    │   │
│  │ HMI     │  │ Modbus  │  │ PCF8574 │  │ W5500   │   │
│  └─────────┘  └─────────┘  └─────────┘  └─────────┘   │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐                │
│  │ ADC1    │  │ TIM2    │  │ IWDG    │                │
│  │ 传感器  │  │ 编码器  │  │ 看门狗  │                │
│  └─────────┘  └─────────┘  └─────────┘                │
└─────────────────────────────────────────────────────────┘
         │              │              │
         ↓              ↓              ↓
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ PCF8574 #1   │ │ PCF8574 #2   │ │ MAX485       │
│ 8路继电器    │ │ 8路继电器    │ │ RS485总线    │
└──────────────┘ └──────────────┘ └──────────────┘
         │              │              │
         ↓              ↓              ↓
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ M1 M2 M3 M4  │ │ H1 H2 F1 F2  │ │ 远程监控PC   │
│ 传送带电机   │ │ 加热器/风扇  │ │ SCADA系统    │
└──────────────┘ └──────────────┘ └──────────────┘

完整BOM清单

类别 器件 型号/规格 数量 单价 采购链接
主控 MCU开发板 STM32F407VET6核心板 1 ¥35 淘宝/立创商城
扩展 I2C扩展 PCF8574模块 2 ¥3 淘宝
继电器 继电器模块 8路5V继电器板(光耦隔离) 2 ¥18 淘宝
通信 RS485收发器 MAX485模块 1 ¥2 淘宝
网络 以太网模块 W5500模块 1 ¥25 淘宝
HMI 触摸屏 7寸串口屏(UART) 1 ¥120 淘宝(迪文/大彩)
传感器 温度传感器 DS18B20防水探头 2 ¥5 淘宝
传感器 压力传感器 0-1.6MPa压力变送器(4-20mA) 1 ¥45 淘宝
电源 开关电源 12V/5A 1 ¥15 淘宝
电源 DC-DC模块 12V转5V/3A 1 ¥3 淘宝
电源 DC-DC模块 12V转3.3V/1A 1 ¥2 淘宝
机箱 配电箱 400×300×200mm铁箱 1 ¥60 淘宝
接线 端子排 UK-2.5N导轨端子 20 ¥0.5 淘宝
接线 导轨 35mm DIN导轨 1米 1 ¥8 淘宝
线材 电源线 RVV 3×1.5mm² 5米 ¥2/米 五金店
线材 信号线 RVVP 2×0.5mm²屏蔽线 10米 ¥1.5/米 淘宝
保护 空气开关 DZ47-63 C10 2P 1 ¥12 淘宝
保护 熔断器 RT18-32 10A 4 ¥3 淘宝
按钮 急停按钮 LA38-11ZS红色蘑菇头 1 ¥8 淘宝
按钮 选择开关 LA38-11XD 2档旋钮 1 ¥6 淘宝
指示灯 信号灯 AD16-22DS 22mm LED 3 ¥3 淘宝

总成本:约 ¥450

接线图

电源部分:
  AC 220V ──→ 空气开关 ──→ 开关电源12V ──┬──→ 继电器模块(12V线圈)
                                        ├──→ DC-DC 5V ──→ STM32/PCF8574
                                        └──→ DC-DC 3.3V ──→ W5500

控制部分:
  STM32_I2C1_SDA ──→ PCF8574 #1 SDA ──→ PCF8574 #2 SDA
  STM32_I2C1_SCL ──→ PCF8574 #1 SCL ──→ PCF8574 #2 SCL

  PCF8574 #1 P0~P7 ──→ 继电器1~8控制端
  PCF8574 #2 P0~P7 ──→ 继电器9~16控制端

  STM32_UART1_TX ──→ HMI_RX
  STM32_UART1_RX ──→ HMI_TX

  STM32_UART2_TX ──→ MAX485_DI
  STM32_UART2_RX ──→ MAX485_RO
  STM32_PA8 ──→ MAX485_DE/RE

  STM32_SPI1 ──→ W5500 (MOSI/MISO/SCK/CS)

  STM32_PA0 ──→ 温度传感器1(DS18B20)
  STM32_PA1 ──→ 温度传感器2(DS18B20)
  STM32_ADC1_IN2 ──→ 压力变送器(4-20mA转0-3.3V)

负载部分:
  继电器1~4 常开触点 ──→ 传送带电机接触器线圈
  继电器5~6 常开触点 ──→ 加热器接触器线圈
  继电器7~8 常开触点 ──→ 风扇接触器线圈
  继电器9~12 常开触点 ──→ 电磁阀线圈
  继电器13~14 常开触点 ──→ 指示灯

软件实现

主程序架构

// main.c - 主程序
#include "stm32f4xx_hal.h"
#include "relay_control.h"
#include "modbus_rtu.h"
#include "hmi_protocol.h"
#include "sensor_read.h"
#include "automation_logic.h"

// 系统状态
typedef enum {
    MODE_MANUAL,    // 手动模式
    MODE_AUTO,      // 自动模式
    MODE_EMERGENCY  // 急停模式
} SystemMode;

SystemMode system_mode = MODE_MANUAL;
bool emergency_stop = false;

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

    // 初始化外设
    MX_GPIO_Init();
    MX_I2C1_Init();
    MX_UART1_Init();  // HMI
    MX_UART2_Init();  // Modbus
    MX_SPI1_Init();   // W5500
    MX_ADC1_Init();
    MX_TIM2_Init();

    // 初始化功能模块
    relay_init();
    modbus_init();
    hmi_init();
    sensor_init();
    w5500_init();
    iwdg_init();

    // 启动定时器中断(1ms)
    HAL_TIM_Base_Start_IT(&htim2);

    while (1) {
        // 读取急停按钮
        emergency_stop = !HAL_GPIO_ReadPin(ESTOP_GPIO_Port, ESTOP_Pin);
        if (emergency_stop) {
            system_mode = MODE_EMERGENCY;
            relay_set_all(0);  // 断开所有继电器
            continue;
        }

        // 读取模式选择开关
        bool mode_sw = HAL_GPIO_ReadPin(MODE_SW_GPIO_Port, MODE_SW_Pin);
        system_mode = mode_sw ? MODE_AUTO : MODE_MANUAL;

        // 读取传感器
        float temp1 = ds18b20_read(0);
        float temp2 = ds18b20_read(1);
        float pressure = pressure_sensor_read();

        // 执行控制逻辑
        if (system_mode == MODE_AUTO) {
            automation_logic_update(temp1, temp2, pressure);
        }

        // 处理HMI通信
        hmi_process();

        // 处理Modbus通信
        modbus_process_task();

        // 处理以太网通信
        w5500_process();

        // 喂狗
        iwdg_feed();

        HAL_Delay(10);
    }
}

// 1ms定时器中断
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == TIM2) {
        // 更新定时器
        for (int i = 0; i < MAX_TIMERS; i++) {
            TON_update(&timers[i]);
        }

        // 更新计数器
        for (int i = 0; i < MAX_COUNTERS; i++) {
            CTU_update(&counters[i]);
        }
    }
}

自动化控制逻辑

// automation_logic.c - 自动化控制逻辑
#include "automation_logic.h"

typedef enum {
    STATE_IDLE,
    STATE_STARTING,
    STATE_RUNNING,
    STATE_STOPPING,
    STATE_ERROR
} AutoState;

AutoState auto_state = STATE_IDLE;
TON_Timer startup_timers[4];
bool start_button = false;
bool stop_button = false;

void automation_logic_update(float temp1, float temp2, float pressure) {
    // 读取HMI按钮状态
    start_button = hmi_get_button(BTN_START);
    stop_button = hmi_get_button(BTN_STOP);

    switch (auto_state) {
        case STATE_IDLE:
            if (start_button && !stop_button) {
                auto_state = STATE_STARTING;
                // 重置定时器
                for (int i = 0; i < 4; i++) {
                    startup_timers[i].IN = false;
                    startup_timers[i].Q = false;
                    startup_timers[i].ET = 0;
                }
            }
            break;

        case STATE_STARTING:
            // 顺序启动:M1 → 2s → M2 → 2s → M3 → 2s → M4
            startup_timers[0].IN = true;
            startup_timers[0].PT = 0;  // 立即启动M1
            TON_update(&startup_timers[0]);
            if (startup_timers[0].Q) {
                relay_set_safe(0, true);  // M1

                startup_timers[1].IN = true;
                startup_timers[1].PT = 2000;
                TON_update(&startup_timers[1]);
                if (startup_timers[1].Q) {
                    relay_set_safe(1, true);  // M2

                    startup_timers[2].IN = true;
                    startup_timers[2].PT = 2000;
                    TON_update(&startup_timers[2]);
                    if (startup_timers[2].Q) {
                        relay_set_safe(2, true);  // M3

                        startup_timers[3].IN = true;
                        startup_timers[3].PT = 2000;
                        TON_update(&startup_timers[3]);
                        if (startup_timers[3].Q) {
                            relay_set_safe(3, true);  // M4
                            auto_state = STATE_RUNNING;
                        }
                    }
                }
            }

            if (stop_button) {
                auto_state = STATE_STOPPING;
            }
            break;

        case STATE_RUNNING:
            // 温度控制:温度过高启动风扇,温度过低启动加热器
            if (temp1 > 80.0f) {
                relay_set_safe(4, false);  // 关闭加热器H1
                relay_set_safe(6, true);   // 启动风扇F1
            } else if (temp1 < 60.0f) {
                relay_set_safe(6, false);  // 关闭风扇F1
                relay_set_safe(4, true);   // 启动加热器H1
            }

            if (temp2 > 80.0f) {
                relay_set_safe(5, false);  // 关闭加热器H2
                relay_set_safe(7, true);   // 启动风扇F2
            } else if (temp2 < 60.0f) {
                relay_set_safe(7, false);  // 关闭风扇F2
                relay_set_safe(5, true);   // 启动加热器H2
            }

            // 压力控制:压力过高打开泄压阀
            if (pressure > 1.2f) {
                relay_set_safe(8, true);   // 打开泄压阀V1
            } else if (pressure < 0.8f) {
                relay_set_safe(8, false);  // 关闭泄压阀V1
            }

            // 故障检测
            if (temp1 > 100.0f || temp2 > 100.0f || pressure > 1.5f) {
                auto_state = STATE_ERROR;
                relay_set(12, true);  // 点亮故障指示灯
            }

            if (stop_button) {
                auto_state = STATE_STOPPING;
            }
            break;

        case STATE_STOPPING:
            // 逆序停止:M4 → 1s → M3 → 1s → M2 → 1s → M1
            relay_set_safe(3, false);  // M4
            HAL_Delay(1000);
            relay_set_safe(2, false);  // M3
            HAL_Delay(1000);
            relay_set_safe(1, false);  // M2
            HAL_Delay(1000);
            relay_set_safe(0, false);  // M1

            // 关闭所有加热器和风扇
            relay_set_safe(4, false);
            relay_set_safe(5, false);
            relay_set_safe(6, false);
            relay_set_safe(7, false);

            auto_state = STATE_IDLE;
            break;

        case STATE_ERROR:
            // 故障状态:断开所有继电器
            relay_set_all(0);

            // 等待复位按钮
            if (hmi_get_button(BTN_RESET)) {
                relay_set(12, false);  // 熄灭故障指示灯
                auto_state = STATE_IDLE;
            }
            break;
    }

    // 更新HMI显示
    hmi_update_status(auto_state, temp1, temp2, pressure);
}

HMI通信协议

// hmi_protocol.c - 串口屏通信协议
#include "hmi_protocol.h"

// 迪文串口屏协议:帧头(3字节) + 数据 + 帧尾(3字节)
// 帧头:5A A5 长度
// 帧尾:FF FF FF

#define HMI_FRAME_HEAD1  0x5A
#define HMI_FRAME_HEAD2  0xA5

typedef struct {
    uint8_t buttons;  // 按钮状态(位域)
    float temp1, temp2, pressure;
    uint8_t relay_state[2];
} HMI_Data;

HMI_Data hmi_data;

void hmi_send_frame(uint8_t cmd, uint8_t *data, uint16_t len) {
    uint8_t frame[256];
    frame[0] = HMI_FRAME_HEAD1;
    frame[1] = HMI_FRAME_HEAD2;
    frame[2] = len + 3;  // 长度包含命令字
    frame[3] = cmd;
    memcpy(&frame[4], data, len);

    HAL_UART_Transmit(&huart1, frame, len + 4, 100);
}

void hmi_update_status(AutoState state, float temp1, float temp2, float pressure) {
    // 更新文本显示控件(地址0x1000)
    char text[64];
    snprintf(text, sizeof(text), "温度1: %.1f°C\n温度2: %.1f°C\n压力: %.2fMPa",
             temp1, temp2, pressure);

    uint8_t data[128];
    data[0] = 0x10;  // 写变量指令
    data[1] = 0x00;  // 地址高字节
    data[2] = 0x10;  // 地址低字节
    memcpy(&data[3], text, strlen(text));

    hmi_send_frame(0x82, data, strlen(text) + 3);

    // 更新继电器状态图标(地址0x2000~0x200F)
    for (int i = 0; i < 16; i++) {
        data[0] = 0x10;
        data[1] = 0x20 + (i >> 8);
        data[2] = i & 0xFF;
        data[3] = relay_get(i) ? 0x01 : 0x00;
        hmi_send_frame(0x82, data, 4);
    }
}

bool hmi_get_button(uint8_t btn_id) {
    return (hmi_data.buttons >> btn_id) & 0x01;
}

void hmi_process(void) {
    uint8_t rx_buf[256];
    uint16_t rx_len = 0;

    // 接收HMI数据(非阻塞)
    if (HAL_UART_Receive(&huart1, rx_buf, 256, 10) == HAL_OK) {
        if (rx_buf[0] == HMI_FRAME_HEAD1 && rx_buf[1] == HMI_FRAME_HEAD2) {
            uint8_t len = rx_buf[2];
            uint8_t cmd = rx_buf[3];

            if (cmd == 0x83) {  // 按钮事件
                uint16_t addr = (rx_buf[4] << 8) | rx_buf[5];
                uint8_t value = rx_buf[6];

                // 解析按钮
                if (addr >= 0x3000 && addr < 0x3010) {
                    uint8_t btn_id = addr - 0x3000;
                    if (value) hmi_data.buttons |= (1 << btn_id);
                    else       hmi_data.buttons &= ~(1 << btn_id);
                }
            }
        }
    }
}

调试与测试

单元测试

// test_relay.c - 继电器模块测试
void test_relay_basic(void) {
    printf("测试1:单路继电器控制\n");
    for (int i = 0; i < 16; i++) {
        relay_set(i, true);
        HAL_Delay(200);
        assert(relay_get(i) == true);
        relay_set(i, false);
        HAL_Delay(200);
        assert(relay_get(i) == false);
    }
    printf("通过\n");
}

void test_relay_interlock(void) {
    printf("测试2:互锁保护\n");
    uint8_t group1[] = {0, 1};  // M1正转/反转
    interlock_register(group1, 2);

    relay_set_safe(0, true);
    assert(relay_get(0) == true);

    relay_set_safe(1, true);  // 应该先断开CH0
    HAL_Delay(150);
    assert(relay_get(0) == false);
    assert(relay_get(1) == true);
    printf("通过\n");
}

void test_sequence_control(void) {
    printf("测试3:顺序控制\n");
    run_sequence(startup_seq, 4);
    assert(relay_get(0) == true);
    assert(relay_get(1) == true);
    assert(relay_get(2) == true);
    assert(relay_get(3) == true);
    printf("通过\n");
}

集成测试

// test_integration.c - 集成测试
void test_modbus_communication(void) {
    printf("测试4:Modbus通信\n");

    // 模拟主站请求:读取线圈0~15
    uint8_t req[] = {0x01, 0x01, 0x00, 0x00, 0x00, 0x10, 0x3D, 0xC6};
    uint8_t resp[256];
    uint16_t resp_len;

    modbus_process(req, sizeof(req), resp, &resp_len);

    assert(resp[0] == 0x01);  // 从站地址
    assert(resp[1] == 0x01);  // 功能码
    assert(resp[2] == 0x02);  // 字节数
    printf("通过\n");
}

void test_automation_logic(void) {
    printf("测试5:自动化逻辑\n");

    // 模拟温度过高
    automation_logic_update(90.0f, 70.0f, 1.0f);
    assert(relay_get(4) == false);  // 加热器关闭
    assert(relay_get(6) == true);   // 风扇启动

    // 模拟温度过低
    automation_logic_update(50.0f, 70.0f, 1.0f);
    assert(relay_get(4) == true);   // 加热器启动
    assert(relay_get(6) == false);  // 风扇关闭

    printf("通过\n");
}

现场安装与调试

安装步骤

  1. 机械安装:
  2. 将控制柜固定在墙面或支架上
  3. 安装DIN导轨和端子排
  4. 固定主控板、继电器模块、电源模块

  5. 电气接线:

  6. 按接线图连接电源线(注意相线/零线/地线)
  7. 连接控制信号线(使用屏蔽线,远离动力线)
  8. 连接负载线(通过继电器常开触点)
  9. 安装终端电阻(RS485总线两端)

  10. 功能测试:

  11. 上电前检查:万用表测量电源电压、绝缘电阻
  12. 空载测试:不接负载,测试继电器动作
  13. 负载测试:接入实际负载,测试控制逻辑
  14. 通信测试:Modbus Poll读写测试、HMI触摸测试

  15. 参数调整:

  16. 调整顺序控制延时时间
  17. 调整温度/压力阈值
  18. 调整PID参数(如有)
  19. 设置报警限值

常见问题排查

见下一节"常见问题与调试"。

常见问题与调试

1. Modbus CRC校验错误

现象: - 主站报告CRC错误,无法读取从站数据 - 从站偶尔收到错误帧

原因分析: 1. 波特率不匹配(主站115200,从站9600) 2. 数据位/停止位/校验位配置错误 3. RS485总线干扰(未接地、未屏蔽) 4. CRC计算算法错误(字节序、多项式)

解决方法:

// 1. 确认UART配置一致
void uart_config_check(void) {
    // 主站和从站必须完全一致
    huart2.Init.BaudRate = 115200;
    huart2.Init.WordLength = UART_WORDLENGTH_8B;
    huart2.Init.StopBits = UART_STOPBITS_1;
    huart2.Init.Parity = UART_PARITY_NONE;
    HAL_UART_Init(&huart2);
}

// 2. 验证CRC算法
void test_crc(void) {
    uint8_t test_data[] = {0x01, 0x03, 0x00, 0x00, 0x00, 0x08};
    uint16_t crc = modbus_crc16(test_data, 6);
    // 正确结果:0x0C44(低字节0x44,高字节0x0C)
    printf("CRC: 0x%04X (应为0x0C44)\n", crc);
}

// 3. 添加CRC错误计数和日志
uint32_t crc_error_count = 0;

void modbus_process_with_log(uint8_t *req, uint16_t req_len,
                              uint8_t *resp, uint16_t *resp_len) {
    if (req_len < 4) return;

    uint16_t crc_recv = req[req_len-2] | (req[req_len-1] << 8);
    uint16_t crc_calc = modbus_crc16(req, req_len - 2);

    if (crc_recv != crc_calc) {
        crc_error_count++;
        printf("CRC错误 #%lu: 接收=0x%04X, 计算=0x%04X\n",
               crc_error_count, crc_recv, crc_calc);
        // 打印原始数据帮助调试
        printf("原始帧: ");
        for (int i = 0; i < req_len; i++) {
            printf("%02X ", req[i]);
        }
        printf("\n");
        return;
    }

    // 正常处理...
}

硬件检查: - 使用示波器/逻辑分析仪查看波形是否完整 - 检查RS485 A/B线是否接反 - 确认终端电阻已正确安装(120Ω,仅总线两端) - 检查地线连接(所有设备共地)

2. RS485总线冲突

现象: - 多个从站同时响应,数据混乱 - 从站地址冲突 - 总线"卡死",无法通信

原因分析: 1. 多个从站使用相同地址 2. 从站未正确释放总线(DE/RE控制错误) 3. 主站轮询间隔过短,从站来不及响应 4. 总线负载过重(超过32个节点)

解决方法:

// 1. 从站地址管理(通过拨码开关设置)
uint8_t get_modbus_address(void) {
    uint8_t addr = 0;
    // 读取4位拨码开关(支持1~15地址)
    if (HAL_GPIO_ReadPin(ADDR_BIT0_GPIO_Port, ADDR_BIT0_Pin)) addr |= 0x01;
    if (HAL_GPIO_ReadPin(ADDR_BIT1_GPIO_Port, ADDR_BIT1_Pin)) addr |= 0x02;
    if (HAL_GPIO_ReadPin(ADDR_BIT2_GPIO_Port, ADDR_BIT2_Pin)) addr |= 0x04;
    if (HAL_GPIO_ReadPin(ADDR_BIT3_GPIO_Port, ADDR_BIT3_Pin)) addr |= 0x08;

    if (addr == 0) addr = 1;  // 默认地址1
    return addr;
}

// 2. 严格的DE/RE控制时序
void modbus_send_response(uint8_t *resp, uint16_t resp_len) {
    // 使能发送
    HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_SET);
    HAL_Delay(1);  // 等待MAX485切换(典型值120ns,留足余量)

    // 发送数据
    HAL_UART_Transmit(&huart2, resp, resp_len, 100);

    // 等待发送完成(重要!)
    while (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_TC) == RESET);
    HAL_Delay(1);  // 等待最后一个字节完全发出

    // 释放总线
    HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_RESET);
}

// 3. 主站轮询策略(避免冲突)
void master_polling_task(void) {
    for (uint8_t addr = 1; addr <= 16; addr++) {
        // 发送请求
        uint8_t req[] = {addr, 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00};
        uint16_t crc = modbus_crc16(req, 6);
        req[6] = crc & 0xFF;
        req[7] = crc >> 8;

        HAL_UART_Transmit(&huart2, req, 8, 100);

        // 等待响应(超时100ms)
        uint8_t resp[256];
        HAL_StatusTypeDef status = HAL_UART_Receive(&huart2, resp, 256, 100);

        if (status == HAL_OK) {
            // 处理响应
            process_response(resp);
        } else {
            printf("从站%d无响应\n", addr);
        }

        // 轮询间隔(重要!给从站足够时间)
        HAL_Delay(50);
    }
}

总线负载计算: - 标准RS485:最多32个节点(每个节点1个单位负载) - 使用中继器/集线器可扩展到256个节点 - 计算公式:总负载 = 发送器负载 + 接收器负载 × 节点数 - MAX485:发送器1 UL,接收器⅛ UL

3. 继电器触点粘连/烧蚀

现象: - 继电器无法断开(触点粘连) - 触点发黑、凹坑(电弧烧蚀) - 继电器寿命缩短

原因分析: 1. 负载电流超过继电器额定值 2. 感性负载(电机、线圈)产生反向电动势 3. 频繁开关(超过机械寿命) 4. 环境潮湿导致触点氧化

解决方法:

// 1. 软启动(减少冲击电流)
void relay_soft_start(uint8_t ch) {
    // PWM软启动(仅适用于阻性负载)
    for (int duty = 0; duty <= 100; duty += 10) {
        // 使用PWM控制继电器线圈电压
        __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, duty);
        HAL_Delay(10);
    }
    relay_set(ch, true);
}

// 2. 限制开关频率
typedef struct {
    uint32_t last_switch_time;
    uint32_t min_interval_ms;  // 最小开关间隔
    uint32_t switch_count;     // 开关次数统计
} RelayLifetime;

RelayLifetime relay_lifetime[16];

bool relay_set_with_limit(uint8_t ch, bool on) {
    if (ch >= 16) return false;

    uint32_t now = HAL_GetTick();
    RelayLifetime *rl = &relay_lifetime[ch];

    // 检查开关间隔
    if (now - rl->last_switch_time < rl->min_interval_ms) {
        printf("继电器%d开关过于频繁,已限制\n", ch);
        return false;
    }

    // 检查寿命(假设额定寿命10万次)
    if (rl->switch_count > 100000) {
        printf("继电器%d已达寿命,建议更换\n", ch);
        // 可选:禁用该继电器
        return false;
    }

    relay_set(ch, on);
    rl->last_switch_time = now;
    rl->switch_count++;

    return true;
}

// 3. 感性负载保护(硬件方案)
// 在继电器触点并联RC吸收电路或压敏电阻
/*
  继电器触点
    ┌─────┐
    │  NO │───┬─→ 负载
    └─────┘   │
              ├─ 100nF ─┐
              │          │
              ├─ 100Ω ──┤
              │          │
              └──────────┘
              RC吸收电路
*/

硬件改进: - 选用额定电流更大的继电器(留2倍余量) - 使用固态继电器(SSR)替代机械继电器(无触点磨损) - 添加RC吸收电路或压敏电阻(MOV) - 定期清洁触点(使用无水酒精)

寿命预测:

负载类型 额定寿命 实际寿命 改进措施
阻性负载(加热器) 10万次 8~10万次 无需改进
感性负载(电机) 10万次 3~5万次 添加RC吸收
容性负载(电容器) 10万次 2~3万次 添加限流电阻
灯负载(白炽灯) 10万次 1~2万次 使用SSR

4. I2C通信失败

现象: - PCF8574无响应 - 继电器状态不更新 - I2C总线"卡死"

原因分析: 1. I2C地址冲突(两片PCF8574地址相同) 2. 上拉电阻不合适(过大或过小) 3. 线缆过长(超过1米) 4. 电源不稳定(PCF8574复位)

解决方法:

// 1. I2C地址扫描
void i2c_scan(void) {
    printf("扫描I2C总线...\n");
    for (uint8_t addr = 0x20; addr <= 0x27; addr++) {
        if (HAL_I2C_IsDeviceReady(&hi2c1, addr << 1, 3, 100) == HAL_OK) {
            printf("发现设备:0x%02X\n", addr);
        }
    }
}

// 2. I2C错误恢复
void i2c_error_recovery(void) {
    // 检测I2C总线状态
    if (__HAL_I2C_GET_FLAG(&hi2c1, I2C_FLAG_BUSY)) {
        printf("I2C总线忙,尝试恢复...\n");

        // 方法1:软件复位
        HAL_I2C_DeInit(&hi2c1);
        HAL_Delay(10);
        HAL_I2C_Init(&hi2c1);

        // 方法2:时钟脉冲恢复(如果方法1无效)
        // 手动产生9个时钟脉冲,释放被拉低的SDA
        for (int i = 0; i < 9; i++) {
            HAL_GPIO_WritePin(I2C_SCL_GPIO_Port, I2C_SCL_Pin, GPIO_PIN_RESET);
            HAL_Delay(1);
            HAL_GPIO_WritePin(I2C_SCL_GPIO_Port, I2C_SCL_Pin, GPIO_PIN_SET);
            HAL_Delay(1);
        }
    }
}

// 3. 带重试的I2C写入
bool pcf8574_write_retry(uint8_t addr, uint8_t data, uint8_t retries) {
    for (uint8_t i = 0; i < retries; i++) {
        HAL_StatusTypeDef status = HAL_I2C_Master_Transmit(&hi2c1, addr << 1,
                                                            &data, 1, 100);
        if (status == HAL_OK) {
            return true;
        }

        printf("I2C写入失败(尝试%d/%d),错误码:%d\n", i+1, retries, status);

        if (status == HAL_TIMEOUT) {
            i2c_error_recovery();
        }

        HAL_Delay(10);
    }
    return false;
}

硬件检查: - 确认PCF8574地址引脚(A0/A1/A2)设置正确 - 测量上拉电阻值(推荐4.7kΩ,快速模式2.2kΩ) - 缩短I2C线缆长度(<30cm为佳) - 检查电源纹波(使用示波器)

5. 系统死机/看门狗复位

现象: - 系统运行一段时间后死机 - 看门狗频繁复位 - 程序跑飞到非法地址

原因分析: 1. 栈溢出(递归调用、局部变量过大) 2. 野指针访问 3. 中断优先级配置错误 4. 任务执行时间过长,未及时喂狗

解决方法:

// 1. 栈使用监控
void check_stack_usage(void) {
    extern uint32_t _estack;  // 栈顶地址(链接脚本定义)
    uint32_t stack_top = (uint32_t)&_estack;
    uint32_t stack_ptr = __get_MSP();  // 当前栈指针
    uint32_t stack_used = stack_top - stack_ptr;

    printf("栈使用:%lu / %lu 字节 (%.1f%%)\n",
           stack_used, STACK_SIZE, stack_used * 100.0f / STACK_SIZE);

    if (stack_used > STACK_SIZE * 0.8) {
        printf("警告:栈使用率过高!\n");
    }
}

// 2. 硬件故障检测
void hardfault_handler_c(uint32_t *hardfault_args) {
    uint32_t stacked_r0 = hardfault_args[0];
    uint32_t stacked_r1 = hardfault_args[1];
    uint32_t stacked_r2 = hardfault_args[2];
    uint32_t stacked_r3 = hardfault_args[3];
    uint32_t stacked_r12 = hardfault_args[4];
    uint32_t stacked_lr = hardfault_args[5];
    uint32_t stacked_pc = hardfault_args[6];
    uint32_t stacked_psr = hardfault_args[7];

    printf("HardFault!\n");
    printf("R0  = 0x%08lX\n", stacked_r0);
    printf("R1  = 0x%08lX\n", stacked_r1);
    printf("R2  = 0x%08lX\n", stacked_r2);
    printf("R3  = 0x%08lX\n", stacked_r3);
    printf("R12 = 0x%08lX\n", stacked_r12);
    printf("LR  = 0x%08lX\n", stacked_lr);
    printf("PC  = 0x%08lX\n", stacked_pc);  // 故障地址
    printf("PSR = 0x%08lX\n", stacked_psr);

    // 保存到Flash或EEPROM,重启后上报
    save_crash_log(stacked_pc, stacked_lr);

    while (1);  // 停止运行,等待复位
}

// 3. 任务执行时间监控
void task_monitor(void) {
    uint32_t start_time = HAL_GetTick();

    // 执行任务
    relay_control_task();

    uint32_t elapsed = HAL_GetTick() - start_time;
    if (elapsed > 100) {  // 任务超过100ms
        printf("警告:relay_control_task耗时%lums\n", elapsed);
    }
}

// 4. 看门狗喂狗策略优化
void watchdog_feed_strategy(void) {
    static uint32_t last_feed_time = 0;
    uint32_t now = HAL_GetTick();

    // 每500ms喂一次狗(看门狗超时1秒)
    if (now - last_feed_time >= 500) {
        // 检查所有关键任务是否正常
        if (modbus_task_alive && relay_task_alive && sensor_task_alive) {
            iwdg_feed();
            last_feed_time = now;
        } else {
            // 有任务异常,不喂狗,让系统复位
            printf("检测到任务异常,触发看门狗复位\n");
        }
    }
}

6. 电磁干扰(EMI)问题

现象: - 继电器误动作 - 通信数据错误 - MCU复位

解决方法: - 继电器线圈并联续流二极管(1N4007) - 信号线使用屏蔽双绞线,屏蔽层单端接地 - 电源输入端添加共模电感和X/Y电容 - PCB布局:数字地和模拟地分开,单点接地 - 机箱接大地(PE保护地)

7. 温度传感器读数异常

现象: - DS18B20读数为85°C(默认值) - 读数跳变、不稳定

解决方法:

// 1. 增加读取重试
float ds18b20_read_retry(uint8_t ch, uint8_t retries) {
    for (uint8_t i = 0; i < retries; i++) {
        float temp = ds18b20_read(ch);

        // 检查是否为有效值
        if (temp != 85.0f && temp > -55.0f && temp < 125.0f) {
            return temp;
        }

        HAL_Delay(100);
    }

    printf("DS18B20读取失败\n");
    return -999.0f;  // 错误标志
}

// 2. 添加滤波
#define TEMP_FILTER_SIZE  5
float temp_filter_buf[TEMP_FILTER_SIZE];
uint8_t temp_filter_idx = 0;

float ds18b20_read_filtered(uint8_t ch) {
    float temp = ds18b20_read(ch);

    // 存入滤波缓冲区
    temp_filter_buf[temp_filter_idx] = temp;
    temp_filter_idx = (temp_filter_idx + 1) % TEMP_FILTER_SIZE;

    // 计算平均值
    float sum = 0;
    for (int i = 0; i < TEMP_FILTER_SIZE; i++) {
        sum += temp_filter_buf[i];
    }

    return sum / TEMP_FILTER_SIZE;
}

硬件检查: - 确认DS18B20供电电压(3.0~5.5V) - 数据线上拉电阻4.7kΩ - 线缆长度<20米(长距离需降低上拉电阻到2.2kΩ)


附录:Modbus寄存器完整映射表

地址 类型 访问 说明 单位/范围
线圈 0x0000 线圈 读写 继电器CH0 0=断开,1=吸合
线圈 0x0001 线圈 读写 继电器CH1 0=断开,1=吸合
线圈 0x0002 线圈 读写 继电器CH2 0=断开,1=吸合
线圈 0x0003 线圈 读写 继电器CH3 0=断开,1=吸合
线圈 0x0004 线圈 读写 继电器CH4 0=断开,1=吸合
线圈 0x0005 线圈 读写 继电器CH5 0=断开,1=吸合
线圈 0x0006 线圈 读写 继电器CH6 0=断开,1=吸合
线圈 0x0007 线圈 读写 继电器CH7 0=断开,1=吸合
线圈 0x0008~0x000F 线圈 读写 继电器CH8~CH15 同上
离散输入 0x0000 离散输入 只读 数字输入DI0 0/1
离散输入 0x0001 离散输入 只读 数字输入DI1 0/1
保持寄存器 0x0000 寄存器 读写 继电器状态掩码 16位,bit=1表示吸合
保持寄存器 0x0001 寄存器 只读 系统状态 0=正常,1=告警
保持寄存器 0x0002 寄存器 只读 ADC输入1 0~4095
保持寄存器 0x0003 寄存器 只读 ADC输入2 0~4095
保持寄存器 0x0010 寄存器 读写 顺序控制命令 1=启动,2=停止,3=暂停,4=恢复
保持寄存器 0x0011 寄存器 只读 顺序控制状态 0=空闲,1=运行,2=暂停,3=错误,4=完成
保持寄存器 0x0020 寄存器 只读 系统运行时间(秒) 0~65535
保持寄存器 0x0021 寄存器 只读 告警计数 0~65535

Modbus Poll配置示例:

连接设置:
  Port: COM3(RS485转USB适配器)
  Baud Rate: 115200
  Data Bits: 8
  Parity: None
  Stop Bits: 1

读取继电器状态(FC01):
  Slave ID: 1
  Function: 01 Read Coils
  Address: 0
  Length: 16

控制单路继电器(FC05):
  Slave ID: 1
  Function: 05 Write Single Coil
  Address: 0(CH0)
  Value: FF00(吸合)或 0000(断开)

批量控制(FC0F):
  Slave ID: 1
  Function: 0F Write Multiple Coils
  Address: 0
  Length: 16
  Data: 0x00FF(CH0~CH7吸合,CH8~CH15断开)