PLC+Arduino实战第11课:用运行许可,控制一轮电机正反转
PLC+Arduino实战第11课:用运行许可,控制一轮电机正反转
我的思路是典型的“低成本原型验证”
在工业自动化领域,直接上大设备有三个最大的风险:贵、危险、容错率低。如果我直接操作大功率变频器或伺服,一旦接线错位或逻辑死循环,可能会直接烧掉昂贵的设备,甚至导致机械损坏或人员受伤。如果用arduino控制直流电机就完全没这个顾虑。而且arduino的PWM和变频器的实现结果类似。熟悉PWM原理对理解变频器有很大帮助。而arduino的C和工业C#在基础语法上差别不大。如果未来想从创客过渡到PLC工程师,那会在程序设计上相当丝滑。
这节课我们让PLC和Arduino一起控制电机,谁负责启动?谁负责定时?一轮动作完成后,怎样避免自动重复运行?

这一课,我用西门子S7-200 SMART CR40、Arduino UNO、光耦隔离模块和DFRobot DRI0042直流电机驱动器,跑通了一套 **运行许可** 控制流程:
```
按下PLC启动按钮
↓
PLC输出运行许可
↓
Arduino执行:
正转3秒 → 刹车1秒 → 反转3秒 → 停止
↓
许可保持ON,也不重复执行
↓
撤销许可、重新启动,才能进入下一轮
```## 一、系统分工:PLC管许可,Arduino管动作
这次没有让PLC直接控制电机的每一个动作,而是把任务分成两层。
PLC负责:
- 判断启动条件是否满足;
- 接受启动按钮的上升沿;
- 保持运行许可;
- 在允许条件撤销或收到取消信号时,撤销许可。
Arduino负责:
- 读取PLC许可;
- 执行正转、刹车等待、反转的动作顺序;
- 控制DRI0042的方向和PWM;
- 一轮结束后等待下一次有效启动。
const byte PERMIT_PIN = 2;
const byte IN1_PIN = 5;
const byte IN2_PIN = 4;
const byte PWM_PIN = 6;
const byte TEST_PWM = 80;
const unsigned long RUN_MS = 3000;
const unsigned long STOP_MS = 1000;
const unsigned long QUALIFY_MS = 50;
// 第一次保持false:只检查输入,不驱动电机。
const bool MOTOR_ENABLED = true;
// 暂定低电平有效,必须根据实际测试修改。
const byte PERMIT_ACTIVE_LEVEL = LOW;
enum State {
WAIT_OFF,
READY,
FORWARD,
WAIT_REVERSE,
REVERSE
};
State state = WAIT_OFF;
unsigned long entered = 0;
unsigned long inputChanged = 0;
bool lastPermit = false;
int lastRaw = -1;
void motorStop() {
analogWrite(PWM_PIN, 0);
digitalWrite(IN1_PIN, LOW);
digitalWrite(IN2_PIN, LOW);
}
void runMotor(bool forward) {
motorStop();
digitalWrite(IN1_PIN, forward ? HIGH : LOW);
digitalWrite(IN2_PIN, forward ? LOW : HIGH);
analogWrite(PWM_PIN, TEST_PWM);
entered = millis();
}
void setup() {
digitalWrite(IN1_PIN, LOW);
digitalWrite(IN2_PIN, LOW);
digitalWrite(PWM_PIN, LOW);
pinMode(IN1_PIN, OUTPUT);
pinMode(IN2_PIN, OUTPUT);
pinMode(PWM_PIN, OUTPUT);
pinMode(PERMIT_PIN, INPUT_PULLUP);
motorStop();
Serial.begin(9600);
Serial.println("LESSON 11: PLC PERMIT");
Serial.println("Boot: wait for permit OFF before accepting ON.");
if (!MOTOR_ENABLED) {
Serial.println("INPUT TEST ONLY: motor disabled.");
}
lastPermit =
(digitalRead(PERMIT_PIN) == PERMIT_ACTIVE_LEVEL);
inputChanged = millis();
}
void loop() {
unsigned long now = millis();
int raw = digitalRead(PERMIT_PIN);
bool permit = (raw == PERMIT_ACTIVE_LEVEL);
if (raw != lastRaw) {
lastRaw = raw;
Serial.print("D2=");
Serial.print(raw == HIGH ? "HIGH" : "LOW");
Serial.print(" interpreted permit=");
Serial.println(permit ? "ON" : "OFF");
}
if (permit != lastPermit) {
lastPermit = permit;
inputChanged = now;
}
bool stable = (now - inputChanged >= QUALIFY_MS);
if (!MOTOR_ENABLED) {
motorStop();
return;
}
// 串口s只允许停止,不提供绕过PLC的启动命令。
bool stopRequested = false;
while (Serial.available() > 0) {
char c = Serial.read();
if (c == 's' || c == 'S') {
stopRequested = true;
}
}
if (stopRequested) {
motorStop();
state = WAIT_OFF;
inputChanged = now;
Serial.println("SERIAL STOP: wait for permit OFF.");
return;
}
// 许可撤销不等待50ms:本次循环立即撤销驱动命令。
if (!permit) {
motorStop();
if (state == FORWARD ||
state == WAIT_REVERSE ||
state == REVERSE) {
state = WAIT_OFF;
Serial.println("PERMIT LOST: cycle cancelled.");
}
if (state == WAIT_OFF && stable) {
state = READY;
Serial.println("READY: waiting for new permit ON.");
}
return;
}
// 许可ON需稳定50ms,且此前已确认OFF。
if (state == READY && stable) {
runMotor(true);
state = FORWARD;
Serial.println("FORWARD: 3 seconds.");
}
if (state == FORWARD && now - entered >= RUN_MS) {
motorStop();
state = WAIT_REVERSE;
entered = now;
Serial.println("BRAKE: 1 second.");
} else if (state == WAIT_REVERSE &&
now - entered >= STOP_MS) {
runMotor(false);
state = REVERSE;
Serial.println("REVERSE: 3 seconds.");
} else if (state == REVERSE &&
now - entered >= RUN_MS) {
motorStop();
state = WAIT_OFF;
Serial.println("DONE: permit must go OFF before next cycle.");
}
}
整体链路如下:
```
CR40 PLC
│ Q1.5
↓
光耦隔离模块
│ O1
↓
Arduino UNO D2
│ D5、D4、D6
↓
DFRobot DRI0042
↓
直流电机
```## 二、PLC地址与控制逻辑
| 地址 | 用途 |
|---|---|
| I0.0 | 启动按钮,上升沿接受一次启动 |
| I0.1 | 运行允许:ON允许,OFF撤销许可 |
| I0.4 | 取消运行:ON撤销许可 |
| Q1.5 | 发送给Arduino的运行许可 |
| M11.0 | 保存启动按钮上一扫描周期的状态 |
| M11.1 | 启动脉冲 |
| M11.2 | 运行许可保持位 |
启动条件是:
```
I0.1 = ON
I0.4 = OFF
↓
按下I0.0
↓
Q1.5保持ON
```
取消条件是:
```text
I0.1变OFF,或I0.4变ON
↓
Q1.5变OFF
```恢复允许条件后,不会直接恢复许可,仍需要重新按启动按钮。
PLC程序还加入了首次扫描处理:首次扫描撤销许可;如果启动按钮在PLC启动时一直按住,也不会直接接受启动,需要先释放再重新按下。

## 三、硬件接线
### UNO与DRI0042
| Arduino UNO | DRI0042 |
|---|---|
| D5 | IN1 |
| D4 | IN2 |
| D6 | PWM |
| GND | COM |
电机连接DRI0042的OUT1、OUT2,电机电源连接驱动器功率输入端。UNO在本次实验中使用USB供电,DRI0042的5VO不接UNO 5V。
### 光耦与UNO
| 光耦模块输出侧 | Arduino UNO |
|---|---|
| O1 | D2 |
| VCC | 5V |
| GND | GND |
这套接法以模块输入、输出电压规格已经核实为前提。本次实测中,PLC许可ON对应UNO的D2低电平,因此程序采用低电平有效。
模块上的D1是指示灯标识,O1才是第1路输出端,接线时不要混淆。
PLC侧24V不能直接接UNO数字输入。光耦两侧的电源与接地,也应按模块规格区分,不能为了“共地”随意跨接隔离两侧。


## 四、调试中遇到的两个问题
### 1. PLC输出灯亮,光耦却没反应
排查时首先要分清:
```
PLC程序让Q1.5为ON
≠
输出端子一定已有24V
```CR40采用继电器输出,除了程序输出状态,还要核对对应输出组公共端的回路供电。
本次接线重点检查了Q1.5所属组公共端4L的供电。实际接线仍应以自己CPU的端子图为准。
测量时,应以同一路24V电源负极为参考,分别检查:
```
电源正极对负极
公共端对负极
Q1.5对负极
光耦输入1+对1-
```这样才能逐段定位,而不是看见输出灯亮,就跳到Arduino程序里找问题。
### 2. Arduino收到许可,电机仍然不动
串口已经显示:
```
D2=LOW interpreted permit=ON
```但电机没有动作。
原因是程序仍处于输入测试模式:
```
const bool MOTOR_ENABLED = false;
```
这个模式只检查许可输入,不驱动电机。确认输入极性后,改为:
```
const bool MOTOR_ENABLED = true;
```重新上传,再撤销PLC许可并重新启动,动作流程才正式运行。
先验证输入,再开放电机动作,让调试范围清楚了很多。
## 五、Arduino采用单轮状态机,而不是自动循环
接入PLC后,原来的“正转—停止—反转—重复”程序就不适用了。
这一课使用的流程是:
```
上电
↓
等待许可OFF
↓
准备就绪
↓
接受新的许可ON
↓
正转3秒
↓
刹车1秒
↓
反转3秒
↓
停止,等待许可OFF
```程序设计了以下规则:
- UNO启动时许可已经ON,不直接启动。
- 一轮完成后,许可保持ON也不重复执行。
- 许可撤销后,才能重新准备下一轮。
- 运行中许可撤销,取消本轮并撤销驱动命令。
- 使用millis()计时,让主循环持续检查输入。
其中,“运行中撤销许可”的处理已写入程序,但仍需要分别在正转、等待和反转阶段进行现场验证,不能把代码逻辑等同于测试结果。
## 六、实际运行结果
本次已连续完成两轮正常动作,串口记录如下:
```
D2=LOW interpreted permit=ON
FORWARD: 3 seconds.
BRAKE: 1 second.
REVERSE: 3 seconds.
DONE: permit must go OFF before next cycle.
```撤销PLC许可后:
```
D2=HIGH interpreted permit=OFF
READY: waiting for new permit ON.
```再次启动,才执行下一轮。
这验证了正常动作顺序,以及“完成后不自动重复、撤销许可后可重新启动”的基本行为。
## 七、复现实验时的注意事项
建议按以下顺序调试:
1. 不接电机,验证PLC启动、许可保持和取消逻辑。
2. 验证光耦输入、输出电压及有效电平。
3. 用Arduino输入测试模式观察D2。
4. 确认信号正确后,再启用电机动作。
5. 补测运行中取消、重启和断线等异常情况。
这一课真正跑通的,不只是电机正反转,而是“条件判断—许可传递—动作执行—完成等待—重新启动”这一套控制关系。





