在这里插入图片描述
“Arduino BLDC 之 Modbus RTU 触发的工业机器人报警联动控制”可以理解为:以 Modbus RTU 作为工业机器人、PLC、传感器和报警设备之间的统一通信接口,当 Modbus 寄存器中的报警位、故障码、急停状态或安全门状态发生变化时,Arduino 下位机立即执行 BLDC 停机、降额、零力矩、声光报警、蜂鸣器联动、状态字回写等动作,形成“报警触发 → 协议解析 → 安全执行 → 状态反馈”的闭环。
一、主要特点

  1. 报警触发基于寄存器映射,而不是随意串口指令
    Modbus RTU 通常使用保持寄存器、输入寄存器、线圈和离散输入承载状态信息。例如 PLC 写入报警寄存器,Arduino 作为从站读取后执行联动;或者 Arduino 作为从站,由上位机/PLC 下发停机指令。
  2. 联动动作分等级
    不是所有报警都应立即切断动力。常见分级包括:
    提示级:仅点亮指示灯、写入状态字;
    降额级:限制 BLDC 最大速度/电流,降低输出功率;
    停机级:进入零力矩或制动停机;
    急停级:断开使能、切断动力、保持安全状态。
  3. BLDC FOC 提供可控停机能力
    相比简单断电,FOC 可先执行减速斜坡、零力矩释放或制动停机,再断开使能。这样能减少机械冲击、避免负载坠落或传动机构损坏。
  4. 状态字和故障码可双向反馈
    Arduino 不仅接收报警,还可通过 Modbus 输入寄存器回写实际速度、错误代码、电机温度、电池电压、急停状态和通讯状态,方便 PLC/SCADA 判断联动是否生效。
  5. 适合接入声光报警器、蜂鸣器和指示灯
    Modbus RTU 声光报警器可通过寄存器控制颜色、闪烁频率和蜂鸣模式,实现不同报警等级的差异化提示。
  6. 实时性受轮询周期限制
    Modbus RTU 是主从轮询协议,报警响应时间取决于波特率、总线负载和主站轮询间隔。典型配置下可用 10–50 ms 级轮询,但不能把它当作硬实时安全回路。
    二、应用场景
  7. 工业巡检机器人接入产线报警系统
    当产线发生安全门打开、光栅触发、气压不足或设备故障时,PLC 通过 Modbus RTU 下发停机指令,机器人立即停止 BLDC 驱动并回写状态。
  8. 协作机器人末端执行器联动
    机器人遇到异常力矩、碰撞或工具故障时,通过 Modbus 触发末端夹爪、传送带或辅助轴停机,并点亮三色灯。
  9. 多机器人协同停机
    多台机器人或 AGV 通过 Modbus 共享急停/保护停状态,一旦其中一台触发报警,其他设备进入减速或等待状态,避免二次碰撞。
  10. 传送带/滑台/升降机构联动
    Modbus RTU 读取编码器位置、电流和故障码,在异常时停止 BLDC 传送带,并联动报警灯和上位机记录。
  11. 实验室与教学平台
    用于演示 Modbus RTU 寄存器映射、报警状态机、FOC 安全停机、看门狗保护和工业通信调试。
  12. 小型产线改造
    对没有完整安全总线的小型设备,可用 Arduino + Modbus RTU 实现低成本报警联动,再逐步接入 PLC 或 SCADA。
    三、需要注意的事项
  13. 安全停机不能只依赖 Modbus
    Modbus RTU 是通信协议,不具备功能安全认证。急停、安全门、光栅等关键信号应优先通过硬件安全回路或安全继电器处理,Modbus 只作为辅助联动和状态反馈。
  14. 报警响应要有超时和去抖
    寄存器位可能因干扰瞬时跳变。应设置持续有效时间、滞回阈值和通信超时;通信中断超过设定时间也应进入安全状态。
  15. 停机动作必须平滑
    BLDC 不应直接硬断电。应先限制加速度、执行减速斜坡,再进入零力矩或制动停机,防止机械冲击和电流尖峰。
  16. 寄存器地址、字节序和缩放系数必须统一
    例如速度、温度、电压、故障码通常以整数形式传输,需要约定缩放系数。字节序不一致会导致误读报警状态。
  17. RS485 总线需要终端电阻和屏蔽接地
    长距离布线应在总线两端加 120 Ω 终端电阻,使用屏蔽双绞线并单点接地,避免电机驱动噪声干扰 Modbus 通信。
  18. Arduino 算力要合理分配
    ATmega328P 可承担单路/双路 FOC、Modbus RTU 从站和简单报警状态机;若同时运行多关节控制、复杂日志和多路通信,建议迁移到 ESP32-S3、Teensy 4.1、STM32 或采用“PLC/上位机决策 + Arduino 执行”的分层架构。
  19. 控制循环与 Modbus 任务不能互相阻塞
    FOC 电流环应放在固定周期中断中,Modbus 解析放在主循环或低优先级任务中。串口打印、SD 卡写入和复杂日志不应阻塞电机控制。
  20. 报警恢复需要明确流程
    停机后不能自动恢复运行。应要求人工复位、确认故障清除、检查传感器状态,并由 PLC 或操作面板下发恢复指令。
  21. 电源与 EMC 必须隔离
    BLDC 加减速会引起母线波动,可能干扰 RS485 收发器和 MCU。控制电源应独立稳压,动力线、信号线和通信线分开布线。
  22. 日志与追溯要保留
    报警触发时间、寄存器地址、故障码、停机前速度和电流应记录到非易失存储或上位机,便于后续排查。

整体链路是:PLC/传感器/机器人写入 Modbus 报警寄存器 → Arduino 解析寄存器并识别报警等级 → 执行 BLDC 减速、停机、零力矩或降额 → 联动声光报警/指示灯 → 回写状态字、故障码和实际运行数据 → 故障清除后经人工或上位机确认恢复。做原型时,建议按“Modbus RTU 寄存器映射 → 单报警位触发停机 → 分级报警与降额 → FOC 平滑停机 → 状态字回写 → 通信超时保护 → 人工复位恢复”逐步推进。

在这里插入图片描述
1、Modbus 报警触发 + 过流检测 + 紧急制动与继电器联动(基础框架)
适用场景:上位机 PLC 通过 Modbus RTU 写入报警触发寄存器,或本地检测到电机过流超过阈值,Arduino 立即执行 motor.move(0) 紧急停机,同时吸合继电器驱动声光报警器,并将报警状态与电机状态回写 Modbus 寄存器供上位机确认。核心逻辑直接来自搜索结果中“报警 Modbus 寄存器设计 → 联动动作 → 闭环反馈”的三段式结构。

#include <SimpleFOC.h>
#include <ModbusRTU.h>

// BLDC 配置
BLDCMotor motor(7);
BLDCDriver3PWM driver(9, 10, 11, 8);
Encoder encoder(2, 3, 2048);
// 电流传感器(简化:使用电机内部估算电流,实际可外接 SamplingAnalog)
// SamplingAnalog currentSensor(A0);

// 报警继电器
const int alarmRelay = 4;

ModbusRTU modbus;

// ===== 报警寄存器地址映射 =====
#define ADDR_ALARM_TRIGGER 0x2001  // 写:1=触发报警,0=复位
#define ADDR_ALARM_STATUS  0x2002  // 读:0=无报警,1=过流,2=过载,3=外部报警
#define ADDR_CURRENT_LIMIT 0x2003  // 写:电流阈值 (mA)
#define ADDR_MOTOR_STATUS  0x2004  // 读:0=运行,1=停机,2=故障

// 报警状态
int alarmStatus = 0;
int motorStatus = 0;
float currentThreshold = 5000.0;  // 默认 5A

void setup() {
  Serial.begin(115200);
  
  // BLDC 初始化
  motor.linkSensor(&encoder);
  motor.linkDriver(&driver);
  motor.controller = MotionControlType::velocity;
  motor.init();
  motor.initFOC();
  
  // 继电器初始化
  pinMode(alarmRelay, OUTPUT);
  digitalWrite(alarmRelay, LOW);
  
  // Modbus 从站初始化(地址 1)
  modbus.begin(1, Serial, 115200, SERIAL_8N1);
  modbus.addHreg(ADDR_ALARM_TRIGGER, 0);
  modbus.addHreg(ADDR_ALARM_STATUS, 0);
  modbus.addHreg(ADDR_CURRENT_LIMIT, (uint16_t)currentThreshold);
  modbus.addHreg(ADDR_MOTOR_STATUS, 0);
}

void loop() {
  motor.loopFOC();
  modbus.task();  // Modbus 协议处理

  // ===== 本地故障检测 =====
  float actualCurrent = fabs(motor.current.q) * 1000.0;  // 转为 mA
  if (actualCurrent > currentThreshold) {
    alarmStatus = 1;  // 过流
    motorStatus = 2;  // 故障
  }
  
  // ===== 上位机报警指令 =====
  uint16_t triggerVal = modbus.Hreg(ADDR_ALARM_TRIGGER);
  if (triggerVal == 1) {
    alarmStatus = 3;  // 外部报警
    motorStatus = 1;  // 停机
  }
  
  // ===== 报警联动执行 =====
  if (alarmStatus != 0) {
    motor.move(0);  // 紧急制动
    digitalWrite(alarmRelay, HIGH);  // 声光报警
  } else {
    digitalWrite(alarmRelay, LOW);
  }
  
  // ===== 状态回写 =====
  modbus.Hreg(ADDR_ALARM_STATUS, alarmStatus);
  modbus.Hreg(ADDR_MOTOR_STATUS, motorStatus);
  
  // 复位处理
  if (triggerVal == 0 && alarmStatus == 3) {
    alarmStatus = 0;
    motorStatus = 0;
    modbus.Hreg(ADDR_ALARM_TRIGGER, 0);
  }
  
  delay(20);
}

解读:该案例直接映射了搜索结果中报警联动控制的寄存器设计(ADDR_ALARM_TRIGGER/ADDR_ALARM_STATUS/ADDR_CURRENT_LIMIT)与执行逻辑(过流检测→motor.move(0)→继电器输出)。BLDC 的 FOC 电流环提供 motor.current.q 作为过流判据,无需额外电流传感器即可实现基础保护。Modbus 的 modbus.task() 需在主循环中高频调用以维持通信响应。

2、Modbus 多寄存器故障反馈 + 超时重试与 CRC 校验(通信可靠性增强)
适用场景:工业现场电磁干扰强,Modbus 通信可能丢帧或 CRC 校验失败。报警联动系统不能因为一次通信错误就误触发或漏触发。本案例在案例一基础上增加超时重试机制和CRC 校验,确保报警指令的可靠送达与状态反馈的完整性。结构参考搜索结果中“多寄存器读写 + 故障反馈”的工业级扩展逻辑。

#include <SimpleFOC.h>
#include <ModbusRTU.h>

BLDCMotor motor(7);
BLDCDriver3PWM driver(9, 10, 11, 8);
Encoder encoder(2, 3, 2048);

const int alarmRelay = 4;
ModbusRTU modbus;

#define ADDR_ALARM_TRIGGER 0x2001
#define ADDR_ALARM_STATUS  0x2002
#define ADDR_FAULT_CODE    0x2005  // 故障码(详细)
#define ADDR_COMM_ERR_CNT  0x2006  // 通信错误计数

int alarmStatus = 0;
int motorStatus = 0;
uint16_t commErrCount = 0;
unsigned long lastModbusOk = 0;
const unsigned long COMM_TIMEOUT = 500;  // 500ms 无有效帧 → 通信故障

void setup() {
  Serial.begin(115200);
  motor.linkSensor(&encoder);
  motor.linkDriver(&driver);
  motor.controller = MotionControlType::velocity;
  motor.init();
  motor.initFOC();
  
  pinMode(alarmRelay, OUTPUT);
  digitalWrite(alarmRelay, LOW);
  
  modbus.begin(1, Serial, 115200, SERIAL_8N1);
  modbus.addHreg(ADDR_ALARM_TRIGGER, 0);
  modbus.addHreg(ADDR_ALARM_STATUS, 0);
  modbus.addHreg(ADDR_FAULT_CODE, 0);
  modbus.addHreg(ADDR_COMM_ERR_CNT, 0);
  
  lastModbusOk = millis();
}

void loop() {
  motor.loopFOC();
  
  // Modbus 处理(返回非 0 表示有错误)
  uint8_t mbResult = modbus.task();
  if (mbResult != 0) {
    commErrCount++;
    modbus.Hreg(ADDR_COMM_ERR_CNT, commErrCount);
  } else {
    lastModbusOk = millis();
  }
  
  // ===== 通信超时保护 =====
  // 通信中断超过 500ms,视为上位机失联,进入安全停机
  if (millis() - lastModbusOk > COMM_TIMEOUT) {
    alarmStatus = 4;  // 通信故障报警
    motorStatus = 2;
    motor.move(0);
    digitalWrite(alarmRelay, HIGH);
  }
  
  // ===== 报警指令处理(带确认) =====
  uint16_t trigger = modbus.Hreg(ADDR_ALARM_TRIGGER);
  if (trigger == 1) {
    alarmStatus = 3;
    motorStatus = 1;
    motor.move(0);
    digitalWrite(alarmRelay, HIGH);
    // 回写确认:清零 trigger 表示已处理
    modbus.Hreg(ADDR_ALARM_TRIGGER, 0);
  }
  
  // ===== 故障码映射 =====
  uint16_t faultCode = 0;
  if (alarmStatus == 1) faultCode = 0x01;  // 过流
  else if (alarmStatus == 3) faultCode = 0x03;  // 外部
  else if (alarmStatus == 4) faultCode = 0x04;  // 通信
  
  modbus.Hreg(ADDR_ALARM_STATUS, alarmStatus);
  modbus.Hreg(ADDR_FAULT_CODE, faultCode);
  
  delay(10);
}

解读:该案例在基础报警联动上增加了两层可靠性保障。第一层是 modbus.task() 返回值检测与 commErrCount 累积,搜索结果中 Modbus 工业应用强调“CRC 校验能有效检测传输错误”。第二层是 COMM_TIMEOUT 超时保护:如果上位机因总线故障或断电无法发送指令,Arduino 不能无限等待,必须在 500ms 内自主进入安全停机。这一逻辑在搜索结果中未被直接提及,但它是工业报警系统“失效安全”原则的体现——通信中断本身就应该被视为一种报警条件。

3、双轴报警联动 + 分级制动状态机(多轴协同安全)
适用场景:工业机器人多轴系统中,某一轴触发报警时,其他轴不能瞬间全部断电(可能导致机械臂因重力下坠或惯性甩动)。需要分级制动:先减速,再软停,最后抱闸/卸力。本案例借鉴搜索结果中双 BLDC 消防机器人的“分级制动状态机”逻辑,将其与 Modbus 报警触发结合。

#include <SimpleFOC.h>
#include <ModbusRTU.h>

// 双轴 BLDC
BLDCMotor motorL(7), motorR(7);
BLDCDriver3PWM driverL(9, 10, 11, 8), driverR(3, 5, 6, 7);
Encoder encL(18, 19, 2048), encR(20, 21, 2048);

const int brakeL = 24, brakeR = 25;  // 抱闸继电器
const int alarmRelay = 4;

ModbusRTU modbus;
#define ADDR_ALARM_TRIGGER 0x2001
#define ADDR_ALARM_STATUS  0x2002
#define ADDR_BRAKE_STATE   0x2007  // 0=运行, 1=减速, 2=软停, 3=抱闸

// 分级制动状态
enum BrakeState { RUNNING, DECEL, SOFT_STOP, HOLD };
BrakeState brakeState = RUNNING;
unsigned long stateTimer = 0;

float targetSpeed = 5.0;
float currentSpeed = 0;

void setup() {
  Serial.begin(115200);
  
  motorL.linkSensor(&encL); motorL.linkDriver(&driverL);
  motorR.linkSensor(&encR); motorR.linkDriver(&driverR);
  motorL.controller = MotionControlType::velocity;
  motorR.controller = MotionControlType::velocity;
  motorL.init(); motorL.initFOC();
  motorR.init(); motorR.initFOC();
  
  pinMode(brakeL, OUTPUT); pinMode(brakeR, OUTPUT);
  pinMode(alarmRelay, OUTPUT);
  digitalWrite(brakeL, HIGH);  // 释放抱闸
  digitalWrite(brakeR, HIGH);
  digitalWrite(alarmRelay, LOW);
  
  modbus.begin(1, Serial, 115200, SERIAL_8N1);
  modbus.addHreg(ADDR_ALARM_TRIGGER, 0);
  modbus.addHreg(ADDR_ALARM_STATUS, 0);
  modbus.addHreg(ADDR_BRAKE_STATE, 0);
}

void loop() {
  motorL.loopFOC(); motorR.loopFOC();
  modbus.task();
  
  // ===== Modbus 报警触发 =====
  bool alarmTriggered = (modbus.Hreg(ADDR_ALARM_TRIGGER) == 1);
  
  if (alarmTriggered && brakeState == RUNNING) {
    brakeState = DECEL;
    stateTimer = millis();
    digitalWrite(alarmRelay, HIGH);
    modbus.Hreg(ADDR_ALARM_STATUS, 1);
  }
  
  // ===== 分级制动状态机 =====
  switch (brakeState) {
    case RUNNING:
      motorL.move(targetSpeed);
      motorR.move(targetSpeed);
      modbus.Hreg(ADDR_BRAKE_STATE, 0);
      break;
      
    case DECEL:
      // 快速减速
      targetSpeed -= 0.8;
      if (targetSpeed < 0) targetSpeed = 0;
      motorL.move(targetSpeed);
      motorR.move(targetSpeed);
      modbus.Hreg(ADDR_BRAKE_STATE, 1);
      if (targetSpeed < 1.0) {
        brakeState = SOFT_STOP;
        stateTimer = millis();
      }
      break;
      
    case SOFT_STOP:
      // 精细逼近零速
      targetSpeed -= 0.2;
      if (targetSpeed < 0) targetSpeed = 0;
      motorL.move(targetSpeed);
      motorR.move(targetSpeed);
      modbus.Hreg(ADDR_BRAKE_STATE, 2);
      if (targetSpeed < 0.1 && millis() - stateTimer > 200) {
        brakeState = HOLD;
      }
      break;
      
    case HOLD:
      // 卸力 + 抱闸
      motorL.move(0);
      motorR.move(0);
      digitalWrite(brakeL, LOW);  // 抱闸
      digitalWrite(brakeR, LOW);
      modbus.Hreg(ADDR_BRAKE_STATE, 3);
      break;
  }
  
  // ===== 复位 =====
  if (modbus.Hreg(ADDR_ALARM_TRIGGER) == 0 && brakeState == HOLD) {
    brakeState = RUNNING;
    targetSpeed = 5.0;
    digitalWrite(brakeL, HIGH);
    digitalWrite(brakeR, HIGH);
    digitalWrite(alarmRelay, LOW);
    modbus.Hreg(ADDR_ALARM_STATUS, 0);
  }
  
  delay(20);
}

解读:该案例将 Modbus 报警触发与分级制动状态机耦合。搜索结果中双 BLDC 消防机器人的分级制动逻辑(RUNNING→DECEL→SOFT_STOP→HOLD)被直接迁移到报警联动场景。关键设计是:报警触发不直接调用 motor.move(0),而是先经过 DECEL 阶段逐步降低 targetSpeed,避免机械臂在高速运动中突然断电导致惯性冲击。HOLD 状态下才真正抱闸并卸力。Modbus 的 ADDR_BRAKE_STATE 寄存器让上位机可以实时监控制动进度,而非只能看到“报警/未报警”的二值状态。

要点解读

  1. 报警联动的核心是“寄存器映射 + 联动动作 + 状态闭环”三段式,而非单纯触发停机
    搜索结果中该架构的原文描述明确将报警寄存器分为三类:报警触发(写)、报警状态(读)、阈值(读写)。案例一严格遵循这一划分:上位机通过写 ADDR_ALARM_TRIGGER 触发报警,Arduino 执行 motor.move(0) 和继电器动作后,将 alarmStatus 和 motorStatus 写回,上位机读取确认。没有回写确认的报警系统只能“发令”,无法“确认结果”,在工业场景中不可接受。

  2. 本地故障检测与上位机报警指令是两条并行路径,不能只依赖一条
    案例一同时处理 actualCurrent > currentThreshold(本地过流)和 triggerVal == 1(上位机指令)。搜索结果中报警联动文章明确将“电机参数超过设定阈值”和“上位机下发报警指令”并列为触发条件。如果只依赖上位机,通信中断时机器人失去保护;如果只依赖本地,上位机无法在检测到外部异常(如安全光幕触发)时主动停机。两条路径的“或”逻辑是安全底线。

  3. 通信超时本身应被视为报警条件,而非简单的“数据无效”
    案例二引入了 COMM_TIMEOUT = 500ms,超时后自主进入安全停机并置 alarmStatus = 4。搜索结果中 Modbus 工业应用强调“通信参数严格匹配”和“CRC 校验”,但未将超时提升为报警。在报警联动系统中,上位机失联意味着报警指令可能无法送达,状态反馈也无法被确认,此时继续运行的风险高于停机。500ms 的阈值需要根据 Modbus 轮询周期标定:如果上位机正常轮询间隔为 100ms,500ms 超时是合理的。

  4. 多轴系统的报警联动必须分级制动,直接卸力可能造成二次伤害
    案例三的 DECEL → SOFT_STOP → HOLD 状态机直接借鉴了搜索结果中消防机器人的分级制动设计。对于单轴或低惯性系统,motor.move(0) 可以接受;但对于多轴机械臂或高惯性负载,瞬间卸力会导致重力下坠或惯性甩动。分级制动的本质是将“停止”从一个离散动作展开为一个受控过程,使 Modbus 报警触发后系统有足够时间安全过渡到抱闸状态。

  5. Arduino 平台实现 Modbus 报警联动时,串口资源是首要约束
    搜索结果明确指出标准 Arduino Uno 仅有一组硬件串口,调试输出与 Modbus 通信会产生冲突,建议使用 Mega 或 ESP32。案例中的 Serial.begin(115200) 既用于 Modbus 也用于调试输出,在实际部署中应分离:Uno 上无法同时运行调试串口和 Modbus,必须使用 SoftwareSerial(可靠性较低)或换用 Mega/ESP32。此外,modbus.task() 需要高频调用以维持协议响应,而 motor.loopFOC() 也需要高频运行,两者的时序竞争在 Uno 上尤其紧张。推荐使用 ESP32-S3 或 Teensy 4.x,将 Modbus 处理放在一个核心、FOC 放在另一个核心。

在这里插入图片描述
4、BLDC 驱动器做 Modbus RTU 从站 —— 报警字映射 + 本地硬联动
适用硬件:ESP32 + SimpleFOC BLDC + RS485 收发器(MAX3485/ADM2483)+ 声光报警器 + 继电器(切断使能)。

核心思路:驱动器把内部状态折射到一张只读寄存器表里(状态字、报警字 1/2、故障码、实测值),主站只读不猜;报警一旦置位就锁存,必须由主站写确认寄存器才清除。同时本地保留一路硬联动:报警立即拉低使能继电器、点亮塔灯,不等主站下令。

/*
 * 案例4:Modbus RTU 从站 + 报警字 + 锁存确认 + 硬联动
 * 从站地址 0x11,19200 8N1
 */
#include <SimpleFOC.h>

#define RS485_DE  4
HardwareSerial RS485(2);          // ESP32: RX=16 TX=17

BLDCMotor motor = BLDCMotor(7);
BLDCDriver3PWM driver = BLDCDriver3PWM(9,10,11,8);
Encoder encoder = Encoder(2,3,2048);
void onEnc(){ encoder.handleA(); }

const uint8_t SLAVE_ID = 0x11;

// ---------------- 寄存器表 ----------------
// 输入寄存器(0x04 读):只读状态
#define IR_STATUS   0x0000   // bit0 ready bit1 running bit2 alarm bit3 fault
#define IR_ALARM1   0x0001   // 见下方位定义
#define IR_ALARM2   0x0002
#define IR_FAULT    0x0003   // 故障码
#define IR_VEL      0x0004   // 实际速度 ×100
#define IR_CUR      0x0005   // 实际电流 ×100
#define IR_TEMP     0x0006   // 温度 ×10

// 保持寄存器(0x03/0x06 读写):控制
#define HR_CMD      0x0010   // bit0 enable bit1 run bit2 reset
#define HR_TGT_VEL  0x0011   // 目标速度 ×100
#define HR_ACK      0x0012   // 写 1 = 确认并清除已锁存报警
#define HR_LAMP     0x0013   // 塔灯:0 灭 1 绿 2 黄 3 红

// 报警位定义(对外契约,一旦发布不可随意改)
#define AL_OVERCUR   (1<<0)
#define AL_OVERTEMP  (1<<1)
#define AL_STALL     (1<<2)
#define AL_ENCODER   (1<<3)
#define AL_UNDERV    (1<<4)
#define AL_COMMTO    (1<<5)
#define AL_ESTOP     (1<<6)
#define AL_INTERLOCK (1<<7)

uint16_t ir[16];      // 输入寄存器镜像
uint16_t hr[16];      // 保持寄存器镜像

// 报警锁存(置位后必须确认才清)
uint16_t alarm1 = 0, alarm2 = 0;
uint16_t faultCode = 0;
bool alarmLatched = false;

// 硬联动引脚
const int PIN_EN   = 25;   // 使能继电器(低有效)
const int PIN_LAMP = 26;   // 塔灯
const int PIN_BUZZ = 27;

// ---------------- CRC16 (Modbus) ----------------
uint16_t crc16(const 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++)
      crc = (crc & 1) ? ((crc >> 1) ^ 0xA001) : (crc >> 1);
  }
  return crc;
}

// ---------------- 报警触发(内部事件)----------------
void raiseAlarm(uint16_t bit, uint16_t code) {
  if (!(alarm1 & bit) && !(alarm2 & bit)) {
    if (bit == AL_OVERCUR || bit == AL_OVERTEMP) alarm1 |= bit;
    else alarm2 |= bit;
    faultCode = code;
    alarmLatched = true;
    // 硬联动:不等主站,立刻断使能 + 红灯 + 蜂鸣
    digitalWrite(PIN_EN, HIGH);       // 断使能
    digitalWrite(PIN_LAMP, HIGH);
    digitalWrite(PIN_BUZZ, HIGH);
    motor.move(0);
  }
}

// ---------------- 报警检测(1kHz 环内调用)----------------
void checkAlarms() {
  float iA = fabsf(motor.current);
  float tC = 40.0f;                  // 简化:无温度传感器时固定
  float vBus = 24.0f;

  if (iA > 8.0f)          raiseAlarm(AL_OVERCUR, 0x01);
  if (tC > 75.0f)         raiseAlarm(AL_OVERTEMP, 0x02);
  if (fabsf(motor.shaft_velocity) < 0.5f &&
      fabsf(iA) > 5.0f && hr[HR_CMD] & 0x02) raiseAlarm(AL_STALL, 0x03);
  if (fabsf(encoder.getAngle()) > 1e6f)    raiseAlarm(AL_ENCODER, 0x04);
  if (vBus < 18.0f)       raiseAlarm(AL_UNDERV, 0x05);
  if (digitalRead(30) == LOW) raiseAlarm(AL_ESTOP, 0x06);      // 急停输入
  if (digitalRead(31) == LOW) raiseAlarm(AL_INTERLOCK, 0x07);  // 外部联锁
}

// ---------------- 寄存器刷新 ----------------
void refreshRegs() {
  ir[IR_ALARM1] = alarm1;
  ir[IR_ALARM2] = alarm2;
  ir[IR_FAULT]  = faultCode;
  ir[IR_VEL]    = (uint16_t)constrain(fabsf(motor.shaft_velocity)*100.0f, 0, 65535);
  ir[IR_CUR]    = (uint16_t)constrain(fabsf(motor.current)*100.0f, 0, 65535);
  ir[IR_TEMP]   = 400;

  uint16_t st = 0;
  if (motor.current_limit > 0) st |= 0x01;                    // ready
  if (motor.target != 0)       st |= 0x02;                    // running
  if (alarm1 || alarm2)        st |= 0x04;                    // alarm
  if (alarmLatched)            st |= 0x08;                    // fault
  ir[IR_STATUS] = st;
}

// ---------------- 异常响应 ----------------
void sendException(uint8_t fc, uint8_t code) {
  uint8_t buf[5] = {SLAVE_ID, (uint8_t)(fc | 0x80), code, 0, 0};
  uint16_t c = crc16(buf, 3);
  buf[3] = c & 0xFF; buf[4] = c >> 8;
  digitalWrite(RS485_DE, HIGH);
  RS485.write(buf, 5);
  RS485.flush();
  digitalWrite(RS485_DE, LOW);
}

// ---------------- 处理一帧 ----------------
void handleFrame(uint8_t* f, uint8_t len) {
  if (len < 8) return;
  if (f[0] != SLAVE_ID) return;

  uint16_t crcCalc = crc16(f, len - 2);
  uint16_t crcRecv = f[len-2] | (f[len-1] << 8);
  if (crcCalc != crcRecv) return;         // CRC 错:静默丢弃(Modbus 规范)

  uint8_t fc = f[1];

  if (fc == 0x04) {                        // 读输入寄存器
    uint16_t addr = (f[2]<<8) | f[3];
    uint16_t num  = (f[4]<<8) | f[5];
    if (addr + num > 16 || num == 0) { sendException(fc, 0x02); return; }

    refreshRegs();
    uint8_t out[64]; uint8_t n = 0;
    out[n++] = SLAVE_ID; out[n++] = 0x04; out[n++] = num*2;
    for (uint16_t i = 0; i < num; i++) {
      out[n++] = ir[addr+i] >> 8;
      out[n++] = ir[addr+i] & 0xFF;
    }
    uint16_t c = crc16(out, n);
    out[n++] = c & 0xFF; out[n++] = c >> 8;
    digitalWrite(RS485_DE, HIGH);
    RS485.write(out, n); RS485.flush();
    digitalWrite(RS485_DE, LOW);
  }
  else if (fc == 0x03) {                   // 读保持寄存器
    uint16_t addr = (f[2]<<8) | f[3];
    uint16_t num  = (f[4]<<8) | f[5];
    if (addr + num > 16 || num == 0) { sendException(fc, 0x02); return; }

    uint8_t out[64]; uint8_t n = 0;
    out[n++] = SLAVE_ID; out[n++] = 0x03; out[n++] = num*2;
    for (uint16_t i = 0; i < num; i++) {
      out[n++] = hr[addr+i] >> 8;
      out[n++] = hr[addr+i] & 0xFF;
    }
    uint16_t c = crc16(out, n);
    out[n++] = c & 0xFF; out[n++] = c >> 8;
    digitalWrite(RS485_DE, HIGH);
    RS485.write(out, n); RS485.flush();
    digitalWrite(RS485_DE, LOW);
  }
  else if (fc == 0x06) {                   // 写单个保持寄存器
    uint16_t addr = (f[2]<<8) | f[3];
    uint16_t val  = (f[4]<<8) | f[5];
    if (addr >= 16) { sendException(fc, 0x02); return; }

    // 有报警未确认时,只允许写 ACK / LAMP,其余拒绝
    if (alarmLatched && addr != HR_ACK && addr != HR_LAMP) {
      sendException(fc, 0x04); return;
    }
    hr[addr] = val;

    if (addr == HR_ACK && val == 1) {
      // 确认:清报警,但保留故障码供追溯
      alarm1 = 0; alarm2 = 0; alarmLatched = false;
      digitalWrite(PIN_LAMP, LOW);
      digitalWrite(PIN_BUZZ, LOW);
      hr[HR_ACK] = 0;
    }
    if (addr == HR_LAMP) {
      digitalWrite(PIN_LAMP, val ? HIGH : LOW);
    }
    if (addr == HR_CMD && !alarmLatched) {
      bool en = val & 0x01, run = val & 0x02;
      digitalWrite(PIN_EN, en ? LOW : HIGH);
      if (!run) motor.move(0);
    }
    if (addr == HR_TGT_VEL && !alarmLatched) {
      motor.move(constrain(val/100.0f, -3.0f, 3.0f));
    }

    // 回显(0x06 响应即请求原样返回)
    digitalWrite(RS485_DE, HIGH);
    RS485.write(f, 8); RS485.flush();
    digitalWrite(RS485_DE, LOW);
  }
  else {
    sendException(fc, 0x01);               // Illegal Function
  }
}

// ---------------- 帧接收(3.5 字符静默判帧)----------------
uint8_t rxBuf[64]; uint8_t rxLen = 0;
unsigned long lastByteT = 0;
const uint16_t T35_MS = 3;                 // 19200bps 下 3.5 字符 ≈ 2ms

void setup() {
  pinMode(PIN_EN, OUTPUT); digitalWrite(PIN_EN, HIGH);   // 上电默认断使能
  pinMode(PIN_LAMP, OUTPUT); pinMode(PIN_BUZZ, OUTPUT);
  pinMode(30, INPUT_PULLUP); pinMode(31, INPUT_PULLUP);

  encoder.init(); encoder.enableInterrupts(onEnc);
  motor.linkSensor(&encoder);
  motor.controller = MotionControlType::velocity;
  motor.PID_velocity.P = 0.3f; motor.PID_velocity.I = 3.0f;
  motor.voltage_limit = 24.0f; motor.current_limit = 8.0f;
  motor.init();
  driver.voltage_power_supply = 24.0f; driver.init();
  motor.linkDriver(&driver);
  motor.initFOC();

  pinMode(RS485_DE, OUTPUT); digitalWrite(RS485_DE, LOW);
  RS485.begin(19200, SERIAL_8N1, 16, 17);
}

void loop() {
  // ---- 收帧 ----
  while (RS485.available()) {
    uint8_t b = RS485.read();
    if (rxLen < sizeof(rxBuf)) rxBuf[rxLen++] = b;
    lastByteT = millis();
  }
  if (rxLen > 0 && millis() - lastByteT > T35_MS) {
    handleFrame(rxBuf, rxLen);
    rxLen = 0;
  }

  // ---- 报警检测 + 执行 ----
  checkAlarms();
  motor.loopFOC();
  if (!alarmLatched) motor.move(motor.target);

  // ---- 通信超时监测:主站 2s 未访问则本地报通信超时 ----
  static unsigned long lastAccess = millis();
  if (rxLen) lastAccess = millis();
  if (millis() - lastAccess > 2000) raiseAlarm(AL_COMMTO, 0x08);

  delay(1);
}

关键点:raiseAlarm 里硬联动先于任何协议处理执行——断使能、点红灯、蜂鸣这三个动作是直接写 IO 的,不依赖主站是否在线。报警联动的第一层必须是本地闭环,Modbus 只是把状态“报上去”,不能是“等下来才动”。

5、主站轮询多台驱动器 —— 报警聚合 + 通信失效即报警 + 联动停机
适用硬件:ESP32(主站) + RS485 总线 + 4 台驱动器(地址 1~4) + 塔灯 + 安全继电器。

核心思路:主站按固定节拍轮询各从站的报警寄存器;通信超时本身作为一级报警(不只是“读不到数据”);报警聚合后按严重度分级,最高级触发全总线联锁停机。轮询、聚合、执行三层分开,避免通信阻塞拖累联动响应。

/*
 * 案例5:主站轮询 + 报警聚合 + 分级联动
 * 从站 1~4,19200 8N1,轮询周期 100ms
 */
#include <Arduino.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

HardwareSerial RS485(2);
#define RS485_DE 4

const uint8_t N_SLAVE = 4;
const uint8_t ADDRS[N_SLAVE] = {1, 2, 3, 4};
const uint16_t IR_ALARM1 = 0x0001;
const uint16_t IR_STATUS = 0x0000;
const uint16_t HR_CMD    = 0x0010;

// ---- 从站状态 ----
struct SlaveState {
  uint16_t alarm1, alarm2, status;
  bool online;
  uint8_t errCnt;
  unsigned long lastOk;
};
SlaveState sl[N_SLAVE];

// ---- 系统报警等级 ----
enum Sev { SEV_OK = 0, SEV_WARN = 1, SEV_STOP = 2, SEV_ESTOP = 3 };
volatile Sev g_sev = SEV_OK;
volatile uint8_t g_faultSlave = 0;
volatile uint16_t g_faultBits = 0;

// ---- 共享 ----
SemaphoreHandle_t mux;

uint16_t crc16(const 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++)
      crc = (crc & 1) ? ((crc >> 1) ^ 0xA001) : (crc >> 1);
  }
  return crc;
}

// ---- 读一个从站的两个输入寄存器 ----
bool readSlave(uint8_t addr, uint16_t reg, uint16_t &v1, uint16_t &v2,
               uint16_t timeoutMs = 30) {
  uint8_t req[8] = {addr, 0x04,
                    (uint8_t)(reg>>8), (uint8_t)(reg&0xFF), 0, 2, 0, 0};
  uint16_t c = crc16(req, 6);
  req[6] = c & 0xFF; req[7] = c >> 8;

  digitalWrite(RS485_DE, HIGH);
  RS485.write(req, 8); RS485.flush();
  digitalWrite(RS485_DE, LOW);

  unsigned long t0 = millis();
  uint8_t resp[16]; uint8_t n = 0;
  while (millis() - t0 < timeoutMs) {
    while (RS485.available() && n < sizeof(resp)) resp[n++] = RS485.read();
    if (n >= 9) break;
  }
  if (n < 9) return false;
  if (resp[1] & 0x80) return false;                    // 异常响应
  uint16_t cc = crc16(resp, n - 2);
  uint16_t cr = resp[n-2] | (resp[n-1] << 8);
  if (cc != cr) return false;

  v1 = (resp[3] << 8) | resp[4];
  v2 = (resp[5] << 8) | resp[6];
  return true;
}

// ---- 写单个保持寄存器 ----
bool writeSlave(uint8_t addr, uint16_t reg, uint16_t val) {
  uint8_t req[8] = {addr, 0x06,
                    (uint8_t)(reg>>8), (uint8_t)(reg&0xFF),
                    (uint8_t)(val>>8), (uint8_t)(val&0xFF), 0, 0};
  uint16_t c = crc16(req, 6);
  req[6] = c & 0xFF; req[7] = c >> 8;
  digitalWrite(RS485_DE, HIGH);
  RS485.write(req, 8); RS485.flush();
  digitalWrite(RS485_DE, LOW);

  unsigned long t0 = millis();
  while (millis() - t0 < 30) {
    if (RS485.available() >= 8) {
      uint8_t r[8];
      for (int i = 0; i < 8; i++) r[i] = RS485.read();
      return (r[0] == addr && r[1] == 0x06);
    }
  }
  return false;
}

// ---- 任务1:轮询 10Hz ----
void taskPoll(void*) {
  TickType_t last = xTaskGetTickCount();
  for (;;) {
    for (int i = 0; i < N_SLAVE; i++) {
      uint16_t a1 = 0, st = 0;
      bool ok = readSlave(ADDRS[i], IR_ALARM1, a1, st);

      xSemaphoreTake(mux, portMAX_DELAY);
      if (ok) {
        sl[i].alarm1 = a1;
        sl[i].status = st;
        sl[i].online = true;
        sl[i].errCnt = 0;
        sl[i].lastOk = millis();
      } else {
        if (sl[i].errCnt < 255) sl[i].errCnt++;
        if (sl[i].errCnt >= 3) sl[i].online = false;   // 连续 3 次失败判离线
      }
      xSemaphoreGive(mux);
    }
    vTaskDelayUntil(&last, pdMS_TO_TICKS(100));
  }
}

// ---- 任务2:报警聚合 20Hz ----
void taskAggregate(void*) {
  TickType_t last = xTaskGetTickCount();
  for (;;) {
    Sev sev = SEV_OK;
    uint8_t fs = 0;
    uint16_t fb = 0;

    xSemaphoreTake(mux, portMAX_DELAY);
    for (int i = 0; i < N_SLAVE; i++) {
      if (!sl[i].online) {
        // 通信失效 = 一级报警(不能当成"没消息"忽略)
        if (sev < SEV_STOP) { sev = SEV_STOP; fs = ADDRS[i]; fb = 0xFFFF; }
        continue;
      }
      uint16_t a = sl[i].alarm1;
      if (a == 0) continue;

      Sev s = SEV_WARN;
      if (a & ((1<<0)|(1<<1)|(1<<2))) s = SEV_STOP;      // 过流/过温/堵转
      if (a & ((1<<6)|(1<<7)))        s = SEV_ESTOP;     // 急停/联锁
      if (s > sev) { sev = s; fs = ADDRS[i]; fb = a; }
    }
    xSemaphoreGive(mux);

    g_sev = sev; g_faultSlave = fs; g_faultBits = fb;
    vTaskDelayUntil(&last, pdMS_TO_TICKS(50));
  }
}

// ---- 任务3:联动执行 50Hz ----
void taskLinkage(void*) {
  TickType_t last = xTaskGetTickCount();
  Sev prev = SEV_OK;
  unsigned long tStop = 0;

  for (;;) {
    Sev s = g_sev;

    if (s != prev) {
      if (s >= SEV_STOP) {
        tStop = millis();
        // 阶段1:全部减速到 0(不立刻切断,给机械一个缓冲)
        for (int i = 0; i < N_SLAVE; i++) writeSlave(ADDRS[i], HR_CMD, 0x00);
        Serial.printf("[LINKAGE] sev=%d slave=%d bits=0x%04X\n",
                      s, g_faultSlave, g_faultBits);
      }
      if (s == SEV_ESTOP) {
        // 阶段2:切断使能(硬线优先,这里用继电器)
        digitalWrite(25, HIGH);
      }
      if (s < prev) {
        // 恢复:需人工确认后由外部写 g_sev 才解除
        Serial.println("[LINKAGE] recovered");
      }
      prev = s;
    }

    // 阶段转换:停机 1.5s 后切断使能
    if (s >= SEV_STOP && tStop && millis() - tStop > 1500) {
      digitalWrite(25, HIGH);
      tStop = 0;
    }
    vTaskDelayUntil(&last, pdMS_TO_TICKS(20));
  }
}

void setup() {
  pinMode(RS485_DE, OUTPUT); digitalWrite(RS485_DE, LOW);
  pinMode(25, OUTPUT); digitalWrite(25, LOW);   // 上电即使能(按实际安全逻辑反转)
  RS485.begin(19200, SERIAL_8N1, 16, 17);
  for (int i = 0; i < N_SLAVE; i++) {
    sl[i] = {0,0,0,false,0,0};
  }
  mux = xSemaphoreCreateMutex();
  xTaskCreatePinnedToCore(taskPoll,      "poll", 4096, NULL, 2, NULL, 0);
  xTaskCreatePinnedToCore(taskAggregate, "agg",  4096, NULL, 2, NULL, 0);
  xTaskCreatePinnedToCore(taskLinkage,   "link", 4096, NULL, 3, NULL, 1);
  Serial.begin(115200);
}
void loop() { vTaskDelay(portMAX_DELAY); }

关键点:errCnt >= 3 才判定从站离线——单次 CRC 错就报离线会让系统在电磁干扰下疯狂报警。反过来,一旦判离线,聚合层立刻按 SEV_STOP 处理,而不是“跳过这台继续跑”。通信失效在安全逻辑里必须是“最坏假设”,因为最坏假设就是这台电机正在失控而你不知道。

6、报警联动的分级动作序列 —— 有序降级 + 幂等 + 人工复位
适用硬件:ESP32 + RS485 + 驱动器若干 + 抱闸继电器 + 安全继电器(STO)+ 塔灯三色 + 蜂鸣。

核心思路:工业机器人的报警联动不是“停机”一个动作,而是一条按顺序执行的降级链:减速 → 停止 → 抱闸 → 断使能 → 塔灯红色 + 蜂鸣 → 等待人工确认 → 条件满足才允许复位。每一步都必须是幂等的(重复执行不会出错),且有独立超时(某一步失败就跳到更保守的下一步)。

/*
 * 案例6:分级联动动作序列 + 幂等步骤 + 超时降级 + 人工复位
 * 等级:0 正常 1 告警 2 停止 3 急停
 */
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

// ---- 硬件 ----
const int PIN_BRAKE = 25;     // 抱闸(高=抱)
const int PIN_STO   = 26;     // 安全继电器(高=断使能)
const int PIN_LAMP_R= 27, PIN_LAMP_Y = 32, PIN_LAMP_G = 33;
const int PIN_BUZZ  = 14;
const int PIN_ACK   = 34;     // 人工确认按钮

// ---- 联动步骤 ----
enum Step {
  ST_IDLE = 0,
  ST_DECEL,       // 减速
  ST_STOP,        // 停止
  ST_BRAKE,       // 抱闸
  ST_DISABLE,     // 断 STO
  ST_ALARM,       // 塔灯 + 蜂鸣
  ST_WAIT_ACK,    // 等人工确认
  ST_RESET_CHK,   // 检查复位条件
  ST_READY        // 就绪(可恢复)
};

struct StepDef {
  Step id;
  uint16_t timeoutMs;
  void (*enter)();
  bool (*done)();
};

Step cur = ST_IDLE;
unsigned long stepT0 = 0;
uint8_t curLevel = 0;

// ---- 步骤实现(全部幂等)----
void enterDecel()   { sendCmdAll(0x00); }              // 写控制字:仅使能、不运行
bool doneDecel()    { return allSpeedBelow(0.05f); }    // 速度 < 0.05 m/s
void enterStop()    { sendCmdAll(0x00); }
bool doneStop()     { return allSpeedBelow(0.01f); }
void enterBrake()   { digitalWrite(PIN_BRAKE, HIGH); }
bool doneBrake()    { return true; }                    // 抱闸命令即完成
void enterDisable() { digitalWrite(PIN_STO, HIGH); }
bool doneDisable()  { return true; }
void enterAlarm()   {
  digitalWrite(PIN_LAMP_R, HIGH);
  digitalWrite(PIN_LAMP_Y, LOW);
  digitalWrite(PIN_LAMP_G, LOW);
  digitalWrite(PIN_BUZZ, HIGH);
}
bool doneAlarm()    { return true; }
void enterWaitAck() { /* 保持报警状态,不等 */ }
bool doneWaitAck()  { return digitalRead(PIN_ACK) == LOW; }
void enterResetChk(){ digitalWrite(PIN_BUZZ, LOW); }
bool doneResetChk() { return allSlaveOnline() && allAlarmCleared(); }
void enterReady()   {
  digitalWrite(PIN_LAMP_R, LOW);
  digitalWrite(PIN_LAMP_Y, HIGH);   // 黄色 = 待启动
  digitalWrite(PIN_STO, LOW);
  digitalWrite(PIN_BRAKE, LOW);
}
bool doneReady()    { return true; }

// ---- 步骤表:按等级决定跳过哪些 ----
const StepDef SEQ[] = {
  {ST_DECEL,     800,  enterDecel,    doneDecel},
  {ST_STOP,      1200, enterStop,     doneStop},
  {ST_BRAKE,      200, enterBrake,    doneBrake},
  {ST_DISABLE,    200, enterDisable,  doneDisable},
  {ST_ALARM,      100, enterAlarm,    doneAlarm},
  {ST_WAIT_ACK, 60000, enterWaitAck,  doneWaitAck},
  {ST_RESET_CHK,  300, enterResetChk, doneResetChk},
  {ST_READY,      100, enterReady,    doneReady},
};
const int SEQ_N = sizeof(SEQ)/sizeof(SEQ[0]);

// ---- 启动联动(等级驱动)----
void startLinkage(uint8_t level) {
  curLevel = level;
  cur = ST_DECEL;
  stepT0 = millis();
  SEQ[0].enter();
}

// ---- 序列推进(20Hz 调用)----
void runSequence() {
  if (cur == ST_IDLE) return;

  int idx = -1;
  for (int i = 0; i < SEQ_N; i++) if (SEQ[i].id == cur) { idx = i; break; }
  if (idx < 0) return;

  bool ok = SEQ[idx].done();
  bool timeout = (millis() - stepT0 > SEQ[idx].timeoutMs);

  // 等级 3(急停):跳过减速,直接抱闸 + 断使能
  if (curLevel >= 3 && cur == ST_DECEL) { ok = true; }

  if (ok || timeout) {
    if (timeout) {
      // 某步超时:跳过中间步骤,直接跳到最保守的断开动作
      Serial.printf("[SEQ] step %d timeout, forcing disable\n", cur);
      digitalWrite(PIN_BRAKE, HIGH);
      digitalWrite(PIN_STO, HIGH);
    }
    if (idx + 1 < SEQ_N) {
      cur = SEQ[idx+1].id;
      stepT0 = millis();
      SEQ[idx+1].enter();
    } else {
      cur = ST_IDLE;
    }
  }
}

// ---- 外部报警等级输入 ----
uint8_t readLevel() {
  // 实际来自案例5的聚合结果;此处示意
  extern volatile uint8_t g_sev;
  return g_sev;
}

void setup() {
  pinMode(PIN_BRAKE, OUTPUT); digitalWrite(PIN_BRAKE, LOW);
  pinMode(PIN_STO, OUTPUT);   digitalWrite(PIN_STO, LOW);
  pinMode(PIN_LAMP_R, OUTPUT); pinMode(PIN_LAMP_Y, OUTPUT);
  pinMode(PIN_LAMP_G, OUTPUT);
  pinMode(PIN_BUZZ, OUTPUT);  digitalWrite(PIN_BUZZ, LOW);
  pinMode(PIN_ACK, INPUT_PULLUP);
  digitalWrite(PIN_LAMP_G, HIGH);   // 上电绿灯
  Serial.begin(115200);
}

void loop() {
  uint8_t lv = readLevel();
  if (lv >= 2 && cur == ST_IDLE) startLinkage(lv);
  if (cur == ST_READY) { cur = ST_IDLE; }
  runSequence();
  delay(50);   // 20Hz
}

关键点:timeout 分支的处理方式是“跳过中间步骤,直接抱闸 + 断使能”。这就是联动序列的核心安全思想——每一步都可能失败,失败时必须往更保守的方向跳,而不是原地重试。如果减速超时还在等减速,车可能已经撞上去了。

五点要点解读
一、寄存器表就是接口约定,报警位定义一旦发布不能随意改
Modbus 联动的本质是“两台设备靠一张地址表对话”。报警位(AL_OVERCUR 到 AL_INTERLOCK)一旦被上位机、HMI、SCADA 引用,就变成了对外接口——改一个位的位置,可能让整条产线的报警逻辑错位。工程上的做法是:位定义留足冗余(16 位只用 8 位)、版本号单独占一个寄存器、废弃位永不重用。案例一把报警分成 ALARM1/ALARM2 两个字,就是为了后续扩展时不破坏已有定义。

二、报警必须锁存,必须人工确认才复位
不锁存的报警是“瞬时报警”:过流 10 ms 就消失,主站 100 ms 轮询一次根本读不到,故障被吞掉。但完全自动锁存又会让系统在边界条件下反复启停(电流刚好在阈值上抖动)。所以必须是锁存 + 确认复位:报警置位后保持,直到主站写 HR_ACK;而且确认之后还要检查故障源是否真的消失(案例三的 RESET_CHK)。这条在安全逻辑里没有例外,因为“自动恢复”意味着“自动重新启动一台可能还有故障的机器”。

三、通信超时本身必须是一级报警,不能当成“没消息”
这是 Modbus 联动最容易出的设计错误:从站掉线了,主站只是“这轮没读到数据”,然后继续正常跑。正确做法是把“连续 N 次无响应”直接升级为 SEV_STOP(案例二)。理由很直接:通信中断意味着你对那台设备的状态一无所知,而一无所知在安全逻辑里等价于最坏情况。同时要设 N≥3 的容错,避免电磁干扰下单次 CRC 错就触发停机。

四、联动动作必须有序、幂等、可超时降级
“报警 → 停机”是错的,“报警 → 减速 → 停止 → 抱闸 → 断使能 → 报警灯”才是对的。顺序不能乱:先抱闸再减速会让机械承受冲击,先断使能再抱闸会让电机自由滑行。每一步还必须满足两个条件——幂等(重复执行不出错,因为可能因超时被重试)和有超时(某步卡住必须跳到更保守的下一步)。案例三的 SEQ[] 表就是这个思想的实现:步骤是数据,跳转规则是逻辑,两者分离后可以按等级裁剪(急停跳过减速)。

五、Modbus 是轮询式、非实时的,安全功能不能只依赖它
Modbus RTU 的响应时间取决于轮询周期(案例二 100 ms/轮,4 台就是 25 ms/台),加上帧传输和从站处理,端到端延迟轻松上百毫秒。对“过流保护”“急停”这类要求毫秒级响应的安全功能,Modbus 只能作为“上报通道”,不能作为“动作通道”。所以案例一的报警联动是本地硬线直连(raiseAlarm 里直接拉 IO),案例二的急停走安全继电器,案例三的 STO 是独立硬线。标准做法是:安全功能走硬线或安全总线(PROFIsafe/CIP Safety),Modbus 只负责状态监视、参数下发、事件记录。这条边界划不清,就是工业现场最常见的安全设计缺陷。

请注意:以上案例仅作为思路拓展的参考示例,不保证完全正确、适配所有场景或可直接编译运行。由于硬件平台、实际使用场景、Arduino 版本的差异,均可能影响代码的适配性与使用方法的选择。在实际编程开发时,请务必根据自身硬件配置、使用场景及具体功能需求进行针对性调整,并通过多次实测验证效果;同时需确保硬件接线正确,充分了解所用传感器、执行器等设备的技术规范与核心特性。对于涉及硬件操作的代码,使用前务必核对引脚定义、电平参数等关键信息的准确性与安全性,避免因参数错误导致硬件损坏或运行异常。

在这里插入图片描述

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐